Idea to enhance pgbench by more modes to generate data (multi-TXNs, UNNEST, COPY BINARY)

Started by Boris Mironov2 months ago8 messages
#1Boris Mironov
boris_mironov@outlook.com
1 attachment(s)

Hello hackers,

For some of my specific hardware tests I needed to generate big databases well beyond RAM size. Hence I turned to pgbench tool and its default 2 modes for client- and server-side generation for TPC-B tests. When I use "scale" factor in range of few thousands (eg, 3000 - 5000) data generation phase takes quite some time. I looked at it as opportunity to prove/disprove 2 hypothesises:

*
will INSERT mode work faster if we commit once every "scale" and turn single INSERT into "for" loop with commits for 3 tables in the end of each loop
*
will "INSERT .. SELECT FROM unnest" be faster than "INSERT .. SELECT FROM generate_series"
*
will BINARY mode work faster than TEXT even though we send much more data
*
and so on

As a result of my experiments I produced significant patch for pgbench utility and though that it might be of interest not just for me. Therefore I'm sending draft version of it in diff format for current development tree on GitHub. As of November 11, 2025 I can merge with main branch of the project on GitHub.

Spoiler alert: "COPY FROM BINARY" is significantly faster than current "COPY FROM TEXT"

Would be happy to polish it if there is interest to such change.

Cheers

Attachments:

pgbench.c.diffapplication/octet-stream; name=pgbench.c.diffDownload
diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 1515ed405ba..71aa1d9479f 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -161,7 +161,7 @@ typedef struct socket_set
  * some configurable parameters */
 
 #define DEFAULT_INIT_STEPS "dtgvp"	/* default -I setting */
-#define ALL_INIT_STEPS "dtgGvpf"	/* all possible steps */
+#define ALL_INIT_STEPS "dtgCGiIvpf"	/* all possible steps */
 
 #define LOG_STEP_SECONDS	5	/* seconds between log messages */
 #define DEFAULT_NXACTS	10		/* default nxacts */
@@ -171,6 +171,14 @@ typedef struct socket_set
 #define MIN_ZIPFIAN_PARAM		1.001	/* minimum parameter for zipfian */
 #define MAX_ZIPFIAN_PARAM		1000.0	/* maximum parameter for zipfian */
 
+/* original single transaction server-side method */
+#define GEN_TYPE_INSERT_ORIGINAL	'G'	/* use INSERT .. SELECT generate_series to generate data */
+/* 'one transaction per scale' server-side methods */
+#define GEN_TYPE_INSERT_SERIES		'i'	/* use INSERT .. SELECT generate_series to generate data */
+#define GEN_TYPE_INSERT_UNNEST  	'I'	/* use INSERT .. SELECT unnest to generate data */
+#define GEN_TYPE_COPY_ORIGINAL		'g' /* use COPY .. FROM STDIN .. TEXT to generate data */
+#define GEN_TYPE_COPY_BINARY		'C' /* use COPY .. FROM STDIN .. BINARY to generate data */
+
 static int	nxacts = 0;			/* number of transactions per client */
 static int	duration = 0;		/* duration in seconds */
 static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
@@ -181,6 +189,18 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
  */
 static int	scale = 1;
 
+/*
+ * mode of data generation to use
+ */
+static char	data_generation_type = '?';
+
+/*
+ * COPY FROM BINARY execution buffer
+ */
+#define BIN_COPY_BUF_SIZE	102400				/* maximum buffer size for COPY FROM BINARY */
+static char		*bin_copy_buffer = NULL;		/* buffer for COPY FROM BINARY */
+static int32_t	 bin_copy_buffer_length = 0;	/* current buffer size */
+
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -402,14 +422,15 @@ typedef struct StatsData
 	 *   directly successful transactions (they were successfully completed on
 	 *                                     the first try).
 	 *
-	 * A failed transaction is defined as unsuccessfully retried transactions.
-	 * It can be one of two types:
-	 *
-	 * failed (the number of failed transactions) =
+	 * 'failed' (the number of failed transactions) =
 	 *   'serialization_failures' (they got a serialization error and were not
-	 *                             successfully retried) +
+	 *                        successfully retried) +
 	 *   'deadlock_failures' (they got a deadlock error and were not
-	 *                        successfully retried).
+	 *                        successfully retried) +
+	 *   'other_sql_failures'  (they failed on the first try or after retries
+	 *                        due to a SQL error other than serialization or
+	 *                        deadlock; they are counted as a failed transaction
+	 *                        only when --continue-on-error is specified).
 	 *
 	 * If the transaction was retried after a serialization or a deadlock
 	 * error this does not guarantee that this retry was successful. Thus
@@ -421,7 +442,7 @@ typedef struct StatsData
 	 *
 	 * 'retried' (number of all retried transactions) =
 	 *   successfully retried transactions +
-	 *   failed transactions.
+	 *   unsuccessful retried transactions.
 	 *----------
 	 */
 	int64		cnt;			/* number of successful transactions, not
@@ -440,6 +461,11 @@ typedef struct StatsData
 	int64		deadlock_failures;	/* number of transactions that were not
 									 * successfully retried after a deadlock
 									 * error */
+	int64		other_sql_failures; /* number of failed transactions for
+									 * reasons other than
+									 * serialization/deadlock failure, which
+									 * is counted if --continue-on-error is
+									 * specified */
 	SimpleStats latency;
 	SimpleStats lag;
 } StatsData;
@@ -457,6 +483,7 @@ typedef enum EStatus
 {
 	ESTATUS_NO_ERROR = 0,
 	ESTATUS_META_COMMAND_ERROR,
+	ESTATUS_CONN_ERROR,
 
 	/* SQL errors */
 	ESTATUS_SERIALIZATION_ERROR,
@@ -770,6 +797,7 @@ static int64 total_weight = 0;
 static bool verbose_errors = false; /* print verbose messages of all errors */
 
 static bool exit_on_abort = false;	/* exit when any client is aborted */
+static bool continue_on_error = false;	/* continue after errors */
 
 /* Builtin test scripts */
 typedef struct BuiltinScript
@@ -842,7 +870,8 @@ static int	wait_on_socket_set(socket_set *sa, int64 usecs);
 static bool socket_has_input(socket_set *sa, int fd, int idx);
 
 /* callback used to build rows for COPY during data loading */
-typedef void (*initRowMethod) (PQExpBufferData *sql, int64 curr);
+typedef void (*initRowMethod)		(PQExpBufferData *sql, int64 curr);
+typedef void (*initRowMethodBin)	(PGconn *con, PGresult *res, int64_t curr, int32_t parent);
 
 /* callback functions for our flex lexer */
 static const PsqlScanCallbacks pgbench_callbacks = {
@@ -906,7 +935,10 @@ usage(void)
 		   "                           d: drop any existing pgbench tables\n"
 		   "                           t: create the tables used by the standard pgbench scenario\n"
 		   "                           g: generate data, client-side\n"
-		   "                           G: generate data, server-side\n"
+		   "                           C:   client-side (single TNX) COPY .. FROM STDIN .. BINARY\n"
+		   "                           G: generate data, server-side in single transaction\n"
+		   "                           i:   server-side (multiple TXNs) INSERT .. SELECT generate_series\n"
+		   "                           I:   server-side (multiple TXNs) INSERT .. SELECT unnest\n"
 		   "                           v: invoke VACUUM on the standard tables\n"
 		   "                           p: create primary key indexes on the standard tables\n"
 		   "                           f: create foreign keys between the standard tables\n"
@@ -949,6 +981,7 @@ usage(void)
 		   "  -T, --time=NUM           duration of benchmark test in seconds\n"
 		   "  -v, --vacuum-all         vacuum all four standard tables before tests\n"
 		   "  --aggregate-interval=NUM aggregate data over NUM seconds\n"
+		   "  --continue-on-error      continue running after an SQL error\n"
 		   "  --exit-on-abort          exit when any client is aborted\n"
 		   "  --failures-detailed      report the failures grouped by basic types\n"
 		   "  --log-prefix=PREFIX      prefix for transaction time log file\n"
@@ -1467,6 +1500,7 @@ initStats(StatsData *sd, pg_time_usec_t start)
 	sd->retried = 0;
 	sd->serialization_failures = 0;
 	sd->deadlock_failures = 0;
+	sd->other_sql_failures = 0;
 	initSimpleStats(&sd->latency);
 	initSimpleStats(&sd->lag);
 }
@@ -1516,6 +1550,9 @@ accumStats(StatsData *stats, bool skipped, double lat, double lag,
 		case ESTATUS_DEADLOCK_ERROR:
 			stats->deadlock_failures++;
 			break;
+		case ESTATUS_OTHER_SQL_ERROR:
+			stats->other_sql_failures++;
+			break;
 		default:
 			/* internal error which should never occur */
 			pg_fatal("unexpected error status: %d", estatus);
@@ -3231,11 +3268,43 @@ sendCommand(CState *st, Command *command)
 }
 
 /*
- * Get the error status from the error code.
+ * Read and discard all available results from the connection.
+ */
+static void
+discardAvailableResults(CState *st)
+{
+	PGresult   *res = NULL;
+
+	for (;;)
+	{
+		res = PQgetResult(st->con);
+
+		/*
+		 * Read and discard results until PQgetResult() returns NULL (no more
+		 * results) or a connection failure is detected. If the pipeline
+		 * status is PQ_PIPELINE_ABORTED, more results may still be available
+		 * even after PQgetResult() returns NULL, so continue reading in that
+		 * case.
+		 */
+		if ((res == NULL && PQpipelineStatus(st->con) != PQ_PIPELINE_ABORTED) ||
+			PQstatus(st->con) == CONNECTION_BAD)
+			break;
+
+		PQclear(res);
+	}
+	PQclear(res);
+}
+
+/*
+ * Determine the error status based on the connection status and error code.
  */
 static EStatus
-getSQLErrorStatus(const char *sqlState)
+getSQLErrorStatus(CState *st, const char *sqlState)
 {
+	discardAvailableResults(st);
+	if (PQstatus(st->con) == CONNECTION_BAD)
+		return ESTATUS_CONN_ERROR;
+
 	if (sqlState != NULL)
 	{
 		if (strcmp(sqlState, ERRCODE_T_R_SERIALIZATION_FAILURE) == 0)
@@ -3257,6 +3326,17 @@ canRetryError(EStatus estatus)
 			estatus == ESTATUS_DEADLOCK_ERROR);
 }
 
+/*
+ * Returns true if --continue-on-error is specified and this error allows
+ * processing to continue.
+ */
+static bool
+canContinueOnError(EStatus estatus)
+{
+	return (continue_on_error &&
+			estatus == ESTATUS_OTHER_SQL_ERROR);
+}
+
 /*
  * Process query response from the backend.
  *
@@ -3375,9 +3455,9 @@ readCommandResponse(CState *st, MetaCommand meta, char *varprefix)
 
 			case PGRES_NONFATAL_ERROR:
 			case PGRES_FATAL_ERROR:
-				st->estatus = getSQLErrorStatus(PQresultErrorField(res,
-																   PG_DIAG_SQLSTATE));
-				if (canRetryError(st->estatus))
+				st->estatus = getSQLErrorStatus(st, PQresultErrorField(res,
+																	   PG_DIAG_SQLSTATE));
+				if (canRetryError(st->estatus) || canContinueOnError(st->estatus))
 				{
 					if (verbose_errors)
 						commandError(st, PQresultErrorMessage(res));
@@ -3409,11 +3489,7 @@ readCommandResponse(CState *st, MetaCommand meta, char *varprefix)
 error:
 	PQclear(res);
 	PQclear(next_res);
-	do
-	{
-		res = PQgetResult(st->con);
-		PQclear(res);
-	} while (res);
+	discardAvailableResults(st);
 
 	return false;
 }
@@ -4041,7 +4117,7 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
 					if (PQpipelineStatus(st->con) != PQ_PIPELINE_ON)
 						st->state = CSTATE_END_COMMAND;
 				}
-				else if (canRetryError(st->estatus))
+				else if (canRetryError(st->estatus) || canContinueOnError(st->estatus))
 					st->state = CSTATE_ERROR;
 				else
 					st->state = CSTATE_ABORTED;
@@ -4562,7 +4638,8 @@ static int64
 getFailures(const StatsData *stats)
 {
 	return (stats->serialization_failures +
-			stats->deadlock_failures);
+			stats->deadlock_failures +
+			stats->other_sql_failures);
 }
 
 /*
@@ -4582,6 +4659,8 @@ getResultString(bool skipped, EStatus estatus)
 				return "serialization";
 			case ESTATUS_DEADLOCK_ERROR:
 				return "deadlock";
+			case ESTATUS_OTHER_SQL_ERROR:
+				return "other";
 			default:
 				/* internal error which should never occur */
 				pg_fatal("unexpected error status: %d", estatus);
@@ -4637,6 +4716,7 @@ doLog(TState *thread, CState *st,
 			int64		skipped = 0;
 			int64		serialization_failures = 0;
 			int64		deadlock_failures = 0;
+			int64		other_sql_failures = 0;
 			int64		retried = 0;
 			int64		retries = 0;
 
@@ -4677,10 +4757,12 @@ doLog(TState *thread, CState *st,
 			{
 				serialization_failures = agg->serialization_failures;
 				deadlock_failures = agg->deadlock_failures;
+				other_sql_failures = agg->other_sql_failures;
 			}
-			fprintf(logfile, " " INT64_FORMAT " " INT64_FORMAT,
+			fprintf(logfile, " " INT64_FORMAT " " INT64_FORMAT " " INT64_FORMAT,
 					serialization_failures,
-					deadlock_failures);
+					deadlock_failures,
+					other_sql_failures);
 
 			fputc('\n', logfile);
 
@@ -5120,9 +5202,9 @@ initPopulateTable(PGconn *con, const char *table, int64 base,
  * a blank-padded string in pgbench_accounts.
  */
 static void
-initGenerateDataClientSide(PGconn *con)
+initGenerateDataClientSideText(PGconn *con)
 {
-	fprintf(stderr, "generating data (client-side)...\n");
+	fprintf(stderr, "TEXT mode...\n");
 
 	/*
 	 * we do all of this in one transaction to enable the backend's
@@ -5138,25 +5220,384 @@ initGenerateDataClientSide(PGconn *con)
 	 * already exist
 	 */
 	initPopulateTable(con, "pgbench_branches", nbranches, initBranch);
-	initPopulateTable(con, "pgbench_tellers", ntellers, initTeller);
+	initPopulateTable(con, "pgbench_tellers",  ntellers,  initTeller);
 	initPopulateTable(con, "pgbench_accounts", naccounts, initAccount);
 
 	executeStatement(con, "commit");
 }
 
+
 /*
- * Fill the standard tables with some data generated on the server
- *
- * As already the case with the client-side data generation, the filler
- * column defaults to NULL in pgbench_branches and pgbench_tellers,
- * and is a blank-padded string in pgbench_accounts.
+ * Dumps binary buffer to file (purely for debugging)
  */
 static void
-initGenerateDataServerSide(PGconn *con)
+dumpBufferToFile(char *filename)
+{
+	FILE *file_ptr;
+	size_t bytes_written;
+
+	file_ptr = fopen(filename, "wb");
+	if (file_ptr == NULL)
+	{
+		fprintf(stderr, "Error opening file %s\n", filename);
+		return; // EXIT_FAILURE;
+	}
+
+	bytes_written = fwrite(bin_copy_buffer, 1, bin_copy_buffer_length, file_ptr);
+
+	if (bytes_written != bin_copy_buffer_length)
+	{
+		fprintf(stderr, "Error writing to file or incomplete write\n");
+		fclose(file_ptr);
+		return; // EXIT_FAILURE;
+	}
+
+	fclose(file_ptr);
+}
+
+/*
+ * Save char data to buffer
+ */
+static void
+bufferCharData(char *src, int32_t len)
+{
+	memcpy((char *) bin_copy_buffer + bin_copy_buffer_length, (char *) src, len);
+	bin_copy_buffer_length += len;
+}
+
+/*
+ * Converts platform byte order into network byte order
+ * SPARC doesn't reqire that
+ */
+static void
+bufferData(void *src, int32_t len)
+{
+#ifdef __sparc__
+	bufferCharData(src, len);
+#else
+	if (len == 1)
+		bufferCharData(src, len);
+	else
+		for (int32_t i = 0; i < len; i++)
+		{
+			((char *) bin_copy_buffer + bin_copy_buffer_length)[i] =
+				((char *) src)[len - i - 1];
+		}
+
+	bin_copy_buffer_length += len;
+#endif
+}
+
+/*
+ * adds column counter
+ */
+static void
+addColumnCounter(int16_t n)
+{
+	bufferData((void *) &n, sizeof(n));
+}
+
+/*
+ * adds column with NULL value
+ */
+static void
+addNullColumn()
+{
+	int32_t null = -1;
+	bufferData((void *) &null, sizeof(null));
+}
+
+/*
+ * adds column with int8 value
+ */
+static void
+addInt8Column(int8_t value)
+{
+	int8_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with int16 value
+ */
+static void
+addInt16Column(int16_t value)
+{
+	int16_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with inti32 value
+ */
+static void
+addInt32Column(int32_t value)
+{
+	int32_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with inti64 value
+ */
+static void
+addInt64Column(int64_t value)
+{
+	int64_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with char value
+ */
+static void
+addCharColumn(char *value)
+{
+	int32_t	size = strlen(value);
+	bufferData((void *) &size, sizeof(size));
+	bufferCharData(value, size);
+}
+
+/*
+ * Starts communication with server for COPY FROM BINARY statement
+ */
+static void
+sendBinaryCopyHeader(PGconn *con)
+{
+	char header[] = {'P','G','C','O','P','Y','\n','\377','\r','\n','\0',
+					 '\0','\0','\0','\0',
+					 '\0','\0','\0','\0' };
+
+	PQputCopyData(con, header, 19);
+}
+
+/*
+ * Finishes communication with server for COPY FROM BINARY statement
+ */
+static void
+sendBinaryCopyTrailer(PGconn *con)
+{
+	static char trailer[] = { 0xFF, 0xFF };
+
+	PQputCopyData(con, trailer, 2);
+}
+
+/*
+ * Flashes current buffer over network if needed
+ */
+static void
+flushBuffer(PGconn *con, PGresult *res, int16_t row_len)
+{
+	if (bin_copy_buffer_length + row_len > BIN_COPY_BUF_SIZE)
+	{
+		/* flush current buffer */
+		if (PQresultStatus(res) == PGRES_COPY_IN)
+			PQputCopyData(con, (char *) bin_copy_buffer, bin_copy_buffer_length);
+		bin_copy_buffer_length = 0;
+	}
+}
+
+/*
+ * Sends current branch row to buffer
+ */
+static void
+initBranchBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len =  35 + 2 + 4*3; /* max row size is 32 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(2);
+
+	addInt32Column(curr + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Sends current teller row to buffer
+ */
+static void
+initTellerBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len =  40 + 2 + 4*4; /* max row size is 40 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(3);
+
+	addInt32Column(curr + 1);
+	addInt32Column(curr / parent + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Sends current account row to buffer
+ */
+static void
+initAccountBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len = 250 + 2 + 4*4; /* max row size is 250 for int64 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(3);
+
+	if (scale <= SCALE_32BIT_THRESHOLD)
+		addInt32Column(curr + 1);
+	else
+		addInt64Column(curr);
+
+	addInt32Column(curr / parent + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Universal wrapper for sending data in binary format
+ */
+static void
+initPopulateTableBinary(PGconn *con, char *table, char *columns,
+						int64_t base, initRowMethodBin init_row)
+{
+	int			 n;
+	PGresult	*res;
+	char		 copy_statement[256];
+	const char	*copy_statement_fmt = "copy %s (%s) from stdin (format binary)";
+	int64_t		 total = base * scale;
+
+	bin_copy_buffer_length = 0;
+
+	/* Use COPY with FREEZE on v14 and later for all ordinary tables */
+	if ((PQserverVersion(con) >= 140000) &&
+		get_table_relkind(con, table) == RELKIND_RELATION)
+		copy_statement_fmt = "copy %s (%s) from stdin with (format binary, freeze on)";
+
+	n = pg_snprintf(copy_statement, sizeof(copy_statement), copy_statement_fmt, table, columns);
+	if (n >= sizeof(copy_statement))
+		pg_fatal("invalid buffer size: must be at least %d characters long", n);
+	else if (n == -1)
+		pg_fatal("invalid format string");
+
+	res = PQexec(con, copy_statement);
+
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("unexpected copy in result: %s", PQerrorMessage(con));
+	PQclear(res);
+
+
+	sendBinaryCopyHeader(con);
+
+	for (int64_t i = 0; i < total; i++)
+	{
+		init_row(con, res, i, base);
+	}
+
+	if (PQresultStatus(res) == PGRES_COPY_IN)
+		PQputCopyData(con, (char *) bin_copy_buffer, bin_copy_buffer_length);
+	else
+		fprintf(stderr, "Unexpected mode %d instead of %d\n", PQresultStatus(res), PGRES_COPY_IN);
+
+	sendBinaryCopyTrailer(con);
+
+	if (PQresultStatus(res) == PGRES_COPY_IN)
+	{
+		if (PQputCopyEnd(con, NULL) == 1) /* success */
+		{
+			res = PQgetResult(con);
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				fprintf(stderr, "Error: %s\n", PQerrorMessage(con));
+			PQclear(res);
+		}
+		else
+			fprintf(stderr, "Error: %s\n", PQerrorMessage(con));
+	}
+}
+
+/*
+ * Wrapper for binary data load
+ */
+static void
+initGenerateDataClientSideBinary(PGconn *con)
+{
+
+	fprintf(stderr, "BINARY mode...\n");
+
+	bin_copy_buffer = pg_malloc(BIN_COPY_BUF_SIZE);
+	bin_copy_buffer_length = 0;
+
+	/*
+	 * we do all of this in one transaction to enable the backend's
+	 * data-loading optimizations
+	 */
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	initPopulateTableBinary(con, "pgbench_branches", "bid, bbalance",
+							nbranches, initBranchBinary);
+	initPopulateTableBinary(con, "pgbench_tellers",  "tid, bid, tbalance",
+							ntellers,  initTellerBinary);
+	initPopulateTableBinary(con, "pgbench_accounts", "aid, bid, abalance",
+							naccounts, initAccountBinary);
+
+	executeStatement(con, "commit");
+
+	pg_free(bin_copy_buffer);
+}
+
+/*
+ * Fill the standard tables with some data generated and sent from the client.
+ */
+static void
+initGenerateDataClientSide(PGconn *con)
+{
+	fprintf(stderr, "generating data (client-side) in ");
+
+	switch (data_generation_type)
+	{
+		case GEN_TYPE_COPY_ORIGINAL:
+			initGenerateDataClientSideText(con);
+			break;
+		case GEN_TYPE_COPY_BINARY:
+			initGenerateDataClientSideBinary(con);
+			break;
+	}
+}
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM generate_series
+ * whole dataset in single transaction
+ */
+static void
+generateDataInsertSingleTXN(PGconn *con)
 {
 	PQExpBufferData sql;
 
-	fprintf(stderr, "generating data (server-side)...\n");
+	fprintf(stderr, "via INSERT .. SELECT generate_series... in single TXN\n");
+
 
 	/*
 	 * we do all of this in one transaction to enable the backend's
@@ -5170,27 +5611,166 @@ initGenerateDataServerSide(PGconn *con)
 	initPQExpBuffer(&sql);
 
 	printfPQExpBuffer(&sql,
-					  "insert into pgbench_branches(bid,bbalance) "
+					  "insert into pgbench_branches(bid, bbalance) "
 					  "select bid, 0 "
-					  "from generate_series(1, %d) as bid", nbranches * scale);
+					  "from generate_series(1, %d) as bid",
+					  scale * nbranches);
 	executeStatement(con, sql.data);
 
 	printfPQExpBuffer(&sql,
-					  "insert into pgbench_tellers(tid,bid,tbalance) "
-					  "select tid, (tid - 1) / %d + 1, 0 "
-					  "from generate_series(1, %d) as tid", ntellers, ntellers * scale);
+					  "insert into pgbench_tellers(tid, bid, tbalance) "
+					  "select tid + 1, tid / %d + 1, 0 "
+					  "from generate_series(0, %d) as tid",
+					  ntellers, (scale * ntellers) - 1);
 	executeStatement(con, sql.data);
 
 	printfPQExpBuffer(&sql,
-					  "insert into pgbench_accounts(aid,bid,abalance,filler) "
-					  "select aid, (aid - 1) / %d + 1, 0, '' "
-					  "from generate_series(1, " INT64_FORMAT ") as aid",
-					  naccounts, (int64) naccounts * scale);
+					  "insert into pgbench_accounts(aid, bid, abalance, "
+								   "filler) "
+					  "select aid + 1, aid / %d + 1, 0, '' "
+					  "from generate_series(0, " INT64_FORMAT ") as aid",
+					  naccounts, (int64) (scale * naccounts) - 1);
 	executeStatement(con, sql.data);
 
+	executeStatement(con, "commit");
+
 	termPQExpBuffer(&sql);
+}
+
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM generate_series
+ * One transaction per 'scale'
+ */
+static void
+generateDataInsertSeries(PGconn *con)
+{
+	PQExpBufferData sql;
+
+	fprintf(stderr, "via INSERT .. SELECT generate_series... in multiple TXN(s)\n");
+
+	initPQExpBuffer(&sql);
+
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	executeStatement(con, "commit");
+
+	for (int i = 0; i < scale; i++)
+	{
+		executeStatement(con, "begin");
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_branches(bid, bbalance) "
+						  "values(%d, 0)", i + 1);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_tellers(tid, bid, tbalance) "
+						  "select tid + 1, tid / %d + 1, 0 "
+						  "from generate_series(%d, %d) as tid",
+						  ntellers, i * ntellers, (i + 1) * ntellers - 1);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_accounts(aid, bid, abalance, "
+									   "filler) "
+						  "select aid + 1, aid / %d + 1, 0, '' "
+						  "from generate_series(" INT64_FORMAT ", "
+								INT64_FORMAT ") as aid",
+						  naccounts, (int64) i * naccounts,
+						  (int64) (i + 1) * naccounts - 1);
+		executeStatement(con, sql.data);
+
+		executeStatement(con, "commit");
+	}
+
+	termPQExpBuffer(&sql);
+}
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM unnest
+ * One transaction per 'scale'
+ */
+static void
+generateDataInsertUnnest(PGconn *con)
+{
+	PQExpBufferData sql;
+
+	fprintf(stderr, "via INSERT .. SELECT unnest...\n");
+
+	initPQExpBuffer(&sql);
+
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
 
 	executeStatement(con, "commit");
+
+	for (int s = 0; s < scale; s++)
+	{
+		executeStatement(con, "begin");
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_branches(bid,bbalance) "
+						  "values(%d, 0)", s + 1);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_tellers(tid, bid, tbalance) "
+						  "select unnest(array_agg(s.i order by s.i)) as tid, "
+								  "%d as bid, 0 as tbalance "
+						  "from generate_series(%d, %d) as s(i)",
+						  s + 1, s * ntellers + 1, (s + 1) * ntellers);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "with data as ("
+						  "   select generate_series(" INT64_FORMAT ", "
+							  INT64_FORMAT ") as i) "
+						  "insert into pgbench_accounts(aid, bid, "
+									  "abalance, filler) "
+						  "select unnest(aid), unnest(bid), 0 as abalance, "
+								  "'' as filler "
+						  "from (select array_agg(i+1) aid, "
+									   "array_agg(i/%d + 1) bid from data)",
+						  (int64) s * naccounts + 1,
+						  (int64) (s + 1) * naccounts, naccounts);
+		executeStatement(con, sql.data);
+
+		executeStatement(con, "commit");
+	}
+
+	termPQExpBuffer(&sql);
+}
+
+/*
+ * Fill the standard tables with some data generated on the server
+ *
+ * As already the case with the client-side data generation, the filler
+ * column defaults to NULL in pgbench_branches and pgbench_tellers,
+ * and is a blank-padded string in pgbench_accounts.
+ */
+static void
+initGenerateDataServerSide(PGconn *con)
+{
+	fprintf(stderr, "generating data (server-side) ");
+
+	switch (data_generation_type)
+	{
+		case GEN_TYPE_INSERT_ORIGINAL:
+			generateDataInsertSingleTXN(con);
+			break;
+		case GEN_TYPE_INSERT_SERIES:
+			generateDataInsertSeries(con);
+			break;
+		case GEN_TYPE_INSERT_UNNEST:
+			generateDataInsertUnnest(con);
+			break;
+	}
 }
 
 /*
@@ -5276,6 +5856,8 @@ initCreateFKeys(PGconn *con)
 static void
 checkInitSteps(const char *initialize_steps)
 {
+	char	data_init_type = 0;
+
 	if (initialize_steps[0] == '\0')
 		pg_fatal("no initialization steps specified");
 
@@ -5287,7 +5869,22 @@ checkInitSteps(const char *initialize_steps)
 			pg_log_error_detail("Allowed step characters are: \"" ALL_INIT_STEPS "\".");
 			exit(1);
 		}
+
+		switch (*step)
+		{
+			case 'g':
+			case 'C':
+			case 'G':
+			case 'i':
+			case 'I':
+				data_init_type++;
+				data_generation_type = *step;
+				break;
+		}
 	}
+
+	if (data_init_type > 1)
+		pg_log_error("WARNING! More than one type of server-side data generation is requested");
 }
 
 /*
@@ -5326,10 +5923,13 @@ runInitSteps(const char *initialize_steps)
 				initCreateTables(con);
 				break;
 			case 'g':
+			case 'C':
 				op = "client-side generate";
 				initGenerateDataClientSide(con);
 				break;
 			case 'G':
+			case 'i':
+			case 'I':
 				op = "server-side generate";
 				initGenerateDataServerSide(con);
 				break;
@@ -6319,6 +6919,7 @@ printProgressReport(TState *threads, int64 test_start, pg_time_usec_t now,
 		cur.serialization_failures +=
 			threads[i].stats.serialization_failures;
 		cur.deadlock_failures += threads[i].stats.deadlock_failures;
+		cur.other_sql_failures += threads[i].stats.other_sql_failures;
 	}
 
 	/* we count only actually executed transactions */
@@ -6461,7 +7062,8 @@ printResults(StatsData *total,
 
 	/*
 	 * Remaining stats are nonsensical if we failed to execute any xacts due
-	 * to others than serialization or deadlock errors
+	 * to other than serialization or deadlock errors and --continue-on-error
+	 * is not set.
 	 */
 	if (total_cnt <= 0)
 		return;
@@ -6477,6 +7079,9 @@ printResults(StatsData *total,
 		printf("number of deadlock failures: " INT64_FORMAT " (%.3f%%)\n",
 			   total->deadlock_failures,
 			   100.0 * total->deadlock_failures / total_cnt);
+		printf("number of other failures: " INT64_FORMAT " (%.3f%%)\n",
+			   total->other_sql_failures,
+			   100.0 * total->other_sql_failures / total_cnt);
 	}
 
 	/* it can be non-zero only if max_tries is not equal to one */
@@ -6580,6 +7185,10 @@ printResults(StatsData *total,
 							   sstats->deadlock_failures,
 							   (100.0 * sstats->deadlock_failures /
 								script_total_cnt));
+						printf(" - number of other failures: " INT64_FORMAT " (%.3f%%)\n",
+							   sstats->other_sql_failures,
+							   (100.0 * sstats->other_sql_failures /
+								script_total_cnt));
 					}
 
 					/*
@@ -6739,6 +7348,7 @@ main(int argc, char **argv)
 		{"verbose-errors", no_argument, NULL, 15},
 		{"exit-on-abort", no_argument, NULL, 16},
 		{"debug", no_argument, NULL, 17},
+		{"continue-on-error", no_argument, NULL, 18},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -7092,6 +7702,10 @@ main(int argc, char **argv)
 			case 17:			/* debug */
 				pg_logging_increase_verbosity();
 				break;
+			case 18:			/* continue-on-error */
+				benchmarking_option_set = true;
+				continue_on_error = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -7447,6 +8061,7 @@ main(int argc, char **argv)
 		stats.retried += thread->stats.retried;
 		stats.serialization_failures += thread->stats.serialization_failures;
 		stats.deadlock_failures += thread->stats.deadlock_failures;
+		stats.other_sql_failures += thread->stats.other_sql_failures;
 		latency_late += thread->latency_late;
 		conn_total_duration += thread->conn_duration;
 
#2Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Boris Mironov (#1)
Re: Idea to enhance pgbench by more modes to generate data (multi-TXNs, UNNEST, COPY BINARY)

Hi Boris,

On Wed, Nov 12, 2025 at 3:25 AM Boris Mironov <boris_mironov@outlook.com> wrote:

Hello hackers,

For some of my specific hardware tests I needed to generate big databases well beyond RAM size. Hence I turned to pgbench tool and its default 2 modes for client- and server-side generation for TPC-B tests. When I use "scale" factor in range of few thousands (eg, 3000 - 5000) data generation phase takes quite some time. I looked at it as opportunity to prove/disprove 2 hypothesises:

will INSERT mode work faster if we commit once every "scale" and turn single INSERT into "for" loop with commits for 3 tables in the end of each loop
will "INSERT .. SELECT FROM unnest" be faster than "INSERT .. SELECT FROM generate_series"
will BINARY mode work faster than TEXT even though we send much more data
and so on

As a result of my experiments I produced significant patch for pgbench utility and though that it might be of interest not just for me. Therefore I'm sending draft version of it in diff format for current development tree on GitHub. As of November 11, 2025 I can merge with main branch of the project on GitHub.

Spoiler alert: "COPY FROM BINARY" is significantly faster than current "COPY FROM TEXT"

Would be happy to polish it if there is interest to such change.

Making pgbench data initialization faster at a higher scale is
desirable and the community might be willing to accept such a change.
Running very large benchmarks is becoming common these days. However,
it's not clear what you are proposing and what's the performance
improvement. Answering following question may help: Your patch
implements all the above methods? Do all of them provide performance
improvement? If each of them performs better under certain conditions,
what are those conditions? Is there one method which performs better
than all others, which is it and why not implement just that method?
What performance numbers are we looking at? Can the methods which use
batch commits, also run those batches in parallel?

--
Best Wishes,
Ashutosh Bapat

#3Boris Mironov
boris_mironov@outlook.com
In reply to: Boris Mironov (#1)
Re: Idea to enhance pgbench by more modes to generate data (multi-TXNs, UNNEST, COPY BINARY)

Hi Ashutosh,

If there is one method that is better than all others, community will
be more willing to accept implementation of that one method than
multiple implementations so as to reduce maintenance burden.

Ok then. I'll leave "COPY FROM STDIN BINARY" implementation out of 3 only.
Would you prefer to replace original COPY FROM STDIN TEXT by this
code or add it as new "init-step" (e.g., with code "c")?

I also have noted that current code doesn't prevent pgbench parameter
like "--init-steps=dtgG". It allows to run data generation step twice.
Each of these "g" and "G" will present own timing in status line. Is this
an oversight or intentional?

The code in the patch does not have enough comments. It's hard to
understand the methods just from the code. Each of the generateData*
functions could use a prologue explaining the data generation method
it uses.

To add comments is not a problem at all. So far, it was just "code for myself"
and I was checking if there is any interest in community to include it.

I'm sure that much more testing is required to run this code under different
conditions and hardware to get a better picture. So far it looks very promising.

Sure.

Cheers,
Boris

#4Ashutosh Bapat
ashutosh.bapat.oss@gmail.com
In reply to: Boris Mironov (#3)
Re: Idea to enhance pgbench by more modes to generate data (multi-TXNs, UNNEST, COPY BINARY)

On Fri, Nov 14, 2025 at 8:51 PM Boris Mironov <boris_mironov@outlook.com> wrote:

Hi Ashutosh,

If there is one method that is better than all others, community will
be more willing to accept implementation of that one method than
multiple implementations so as to reduce maintenance burden.

Ok then. I'll leave "COPY FROM STDIN BINARY" implementation out of 3 only.
Would you prefer to replace original COPY FROM STDIN TEXT by this
code or add it as new "init-step" (e.g., with code "c")?

TEXT copy may be useful for cross platform client side data
generation. BINARY might be useful for same platform client side
generation or server side generation. Just a thought, use TEXT or
BINARY automatically based on where it's cross-platform or same
platform setup.

I also have noted that current code doesn't prevent pgbench parameter
like "--init-steps=dtgG". It allows to run data generation step twice.
Each of these "g" and "G" will present own timing in status line. Is this
an oversight or intentional?

I would review the commit a386942bd29b0ef0c9df061392659880d22cdf43 and
the discussion thread
/messages/by-id/alpine.DEB.2.21.1904061826420.3678@lancre
mentioned in the commit message to find that out. At first glance it
looks like an oversight, but I haven't reviewed the commit and thread
myself. That thread might reveal why generate_series() was used
instead of BINARY COPY for server side data generation. If it needs to
change it's better to start a separate thread and separate patch for
that discussion.

--
Best Wishes,
Ashutosh Bapat

#5Boris Mironov
boris_mironov@outlook.com
In reply to: Ashutosh Bapat (#4)
Re: Idea to enhance pgbench by more modes to generate data (multi-TXNs, UNNEST, COPY BINARY)

Hi Ashutosh,

TEXT copy may be useful for cross platform client side data
generation. BINARY might be useful for same platform client side
generation or server side generation. Just a thought, use TEXT or
BINARY automatically based on where it's cross-platform or same
platform setup.

It is true that BINARY format is not as flexible as TEXT. Postgres expects
data in wire to arrive in "network byte order". AFAIK only Solaris can
send its data without byte reordering. I support such exception via
#ifdef __sparc__

I don't see an easy way to make decision within pgbench on which
COPY mode to use TEXT or BINARY except specifically asking for one
via command line parameter. This is why I left flag "g" for TEXT mode
and added BINARY as "C" (for "Copy" and upper case as faster). I guess,
we can add alias "c" for old client-side generation as "copy", but slower
version of it.

While we are on topic of client- vs server-side generation. IMHO these are
quite misleading terms. Both of them are executed by RDBMS on server side,
but "server" one gets quite short query (and quite slow in execution) and
"client" one gets quite big network transfer (and quite fast in execution). The
reason is difference in data path that needs to reflected in documentation.
On top of it server-side thrashes DB cache, while client-side works via ring buffer
that doesn't allocate more than 1/8 of shared_buffers,

I would review the commit a386942bd29b0ef0c9df061392659880d22cdf43 and
the discussion thread
/messages/by-id/alpine.DEB.2.21.1904061826420.3678@lancre
mentioned in the commit message to find that out. At first glance it
looks like an oversight, but I haven't reviewed the commit and thread
myself. That thread might reveal why generate_series() was used
instead of BINARY COPY for server side data generation. If it needs to
change it's better to start a separate thread and separate patch for
that discussion.

Thank you for this hint. I went through whole thread and there they discuss
how to reflect certain behavior of init-steps and nothing about COPY BINARY.
Major point of generate_series() introduction is to send short query to DB
and not to worry about network performance. It is quite true that COPY
sends tons of data over network and it might be an issue for slow network.
They also touched on topic of "one huge transaction" for whole generated
dataset or few smaller transaction.

Allow me to repost my benchmarks here (as it was lost for pgsql-hasckers
because I just used Reply instead of Reply-To-All)

Tests:
Test | Binary | Init mode | Query and details
-----|----------|-----------|-------------------------------------------------------
1 | original | G | INSERT FROM generate_series in single huge transaction
2 | enhanced | G | INSERT FROM generate_series in single huge transaction
3 | enhanced | i | INSERT FROM generate_series in one transaction per scale
4 | enhanced | I | INSERT FROM unnest in one transaction per scale
5 | original | g | COPY FROM STDIN TEXT in single transaction
6 | enhanced | g | COPY FROM STDIN TEXT in single transaction
7 | enhanced | C | COPY FROM STDIN BINARY in single transaction

Test | Scale and seconds to complete data generation        
| 1 | 2 | 10 | 20 | 100 | 200 | 1000 | 2000
-----|------|------|------|------|-------|-------|--------|--------
1 | 0.19 | 0.37 | 2.01 | 4.34 | 22.58 | 46.64 | 245.98 | 525.99
2 | 0.30 | 0.47 | 2.18 | 4.37 | 25.38 | 56.66 | 240.89 | 482.63
3 | 0.18 | 0.39 | 2.14 | 4.19 | 23.78 | 47.63 | 240.91 | 483.19
4 | 0.18 | 0.38 | 2.17 | 4.39 | 23.68 | 47.93 | 242.63 | 487.33
5 | 0.11 | 0.22 | 1.46 | 2.95 | 15.69 | 32.86 | 154.16 | 311.00
6 | 0.11 | 0.22 | 1.43 | 2.89 | 16.01 | 29.41 | 158.10 | 307.54
7 | 0.14 | 0.12 | 0.56 | 1.16 | 6.22 | 12.70 | 64.70 | 135.58

"Original" binary is pgbench v17.6.
"Enhanced" binary is pgbench 19-devel with proposed patch.

As we can see another point of discussion in mentioned earlier
thread on pgsql-hackers said that multi transactions for init-step
do NOT bring any benefit. My numbers show some increase in
performance by simply INSERT-ing data in loop with one COMMIT
per "scale" on lower scales. On higher scales benefit dissapears. My
guess here is quite active process WAL archiver.

COPY TEXT is 36% faster than INSERT with multiple transactions.
COPY BINARY is ~72% faster than INSERT with multiple transactions.

At this point I'm torn between keeping old modes and logic for
backward compatibility and introduction of new modes for INSERT & COPY
versus simply replacing old less efficient logic with new one.

Sorry for quite long response.

Best regards,
Boris

________________________________
From: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Sent: November 16, 2025 11:58 PM
To: Boris Mironov <boris_mironov@outlook.com>
Cc: pgsql-hackers@postgresql.org <pgsql-hackers@postgresql.org>
Subject: Re: Idea to enhance pgbench by more modes to generate data (multi-TXNs, UNNEST, COPY BINARY)

On Fri, Nov 14, 2025 at 8:51 PM Boris Mironov <boris_mironov@outlook.com> wrote:

Hi Ashutosh,

If there is one method that is better than all others, community will
be more willing to accept implementation of that one method than
multiple implementations so as to reduce maintenance burden.

Ok then. I'll leave "COPY FROM STDIN BINARY" implementation out of 3 only.
Would you prefer to replace original COPY FROM STDIN TEXT by this
code or add it as new "init-step" (e.g., with code "c")?

TEXT copy may be useful for cross platform client side data
generation. BINARY might be useful for same platform client side
generation or server side generation. Just a thought, use TEXT or
BINARY automatically based on where it's cross-platform or same
platform setup.

I also have noted that current code doesn't prevent pgbench parameter
like "--init-steps=dtgG". It allows to run data generation step twice.
Each of these "g" and "G" will present own timing in status line. Is this
an oversight or intentional?

I would review the commit a386942bd29b0ef0c9df061392659880d22cdf43 and
the discussion thread
/messages/by-id/alpine.DEB.2.21.1904061826420.3678@lancre
mentioned in the commit message to find that out. At first glance it
looks like an oversight, but I haven't reviewed the commit and thread
myself. That thread might reveal why generate_series() was used
instead of BINARY COPY for server side data generation. If it needs to
change it's better to start a separate thread and separate patch for
that discussion.

--
Best Wishes,
Ashutosh Bapat

#6Boris Mironov
boris_mironov@outlook.com
In reply to: Boris Mironov (#5)
1 attachment(s)
Re: Idea to enhance pgbench by more modes to generate data (multi-TXNs, UNNEST, COPY BINARY)

Hi Ashutosh,

Just wanted to let you know that I've submitted this patch
to CommitFest (see https://commitfest.postgresql.org/patch/6245/)

Interestingly enough there is one more patch from Mircea Cadariu in the same
CommitFest about pgbench (https://commitfest.postgresql.org/patch/6242/)
That patch has been submitted few days ago and is proposing to run
data generation phase in parallel threads. It shows significant
improvements over performance of original single-thread code.

Hopefully sooner or later pgbench will get significant performance
gains in data generation from these two patches.

Original version of my patch failed in GitHub tests. Therefore I have
to start posting updated versions here.

Attached is updated version that sets default value for filler columns.
This trick allows significantly shrink network traffic for COPY FROM BINARY.
Absence of filler column in dataflow has failed my original patch in GitHub
pipeline.

I also switched from one huge transaction for COPY FROM BINARY to
"one per scale". This will simplify merge with multi-threaded data load
proposed by Mircea. Unfortunately, it killed possibility to freeze data right
away, which was possible when table truncation and data load was done
in the same transaction.

I think it would be fair to leave all original modes of data generation
until official review in CommitFest. Hence "INSERT SELECT FROM UNNEST"
is staying so far as there might be interest in community for benchmarking
of columnar tables (eg, for OLAP loads or Timescale DB).

Best regards,
Boris

Attachments:

pgbench.c.diffapplication/octet-stream; name=pgbench.c.diffDownload
diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 1515ed405ba..6b89007a63b 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -161,7 +161,7 @@ typedef struct socket_set
  * some configurable parameters */
 
 #define DEFAULT_INIT_STEPS "dtgvp"	/* default -I setting */
-#define ALL_INIT_STEPS "dtgGvpf"	/* all possible steps */
+#define ALL_INIT_STEPS "dtgCGiIvpf"	/* all possible steps */
 
 #define LOG_STEP_SECONDS	5	/* seconds between log messages */
 #define DEFAULT_NXACTS	10		/* default nxacts */
@@ -171,6 +171,14 @@ typedef struct socket_set
 #define MIN_ZIPFIAN_PARAM		1.001	/* minimum parameter for zipfian */
 #define MAX_ZIPFIAN_PARAM		1000.0	/* maximum parameter for zipfian */
 
+/* original single transaction server-side method */
+#define GEN_TYPE_INSERT_ORIGINAL	'G'	/* use INSERT .. SELECT generate_series to generate data */
+/* 'one transaction per scale' server-side methods */
+#define GEN_TYPE_INSERT_SERIES		'i'	/* use INSERT .. SELECT generate_series to generate data */
+#define GEN_TYPE_INSERT_UNNEST  	'I'	/* use INSERT .. SELECT unnest to generate data */
+#define GEN_TYPE_COPY_ORIGINAL		'g' /* use COPY .. FROM STDIN .. TEXT to generate data */
+#define GEN_TYPE_COPY_BINARY		'C' /* use COPY .. FROM STDIN .. BINARY to generate data */
+
 static int	nxacts = 0;			/* number of transactions per client */
 static int	duration = 0;		/* duration in seconds */
 static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
@@ -181,6 +189,18 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
  */
 static int	scale = 1;
 
+/*
+ * mode of data generation to use
+ */
+static char	data_generation_type = '?';
+
+/*
+ * COPY FROM BINARY execution buffer
+ */
+#define BIN_COPY_BUF_SIZE	102400				/* maximum buffer size for COPY FROM BINARY */
+static char		*bin_copy_buffer = NULL;		/* buffer for COPY FROM BINARY */
+static int32_t	 bin_copy_buffer_length = 0;	/* current buffer size */
+
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -402,14 +422,15 @@ typedef struct StatsData
 	 *   directly successful transactions (they were successfully completed on
 	 *                                     the first try).
 	 *
-	 * A failed transaction is defined as unsuccessfully retried transactions.
-	 * It can be one of two types:
-	 *
-	 * failed (the number of failed transactions) =
+	 * 'failed' (the number of failed transactions) =
 	 *   'serialization_failures' (they got a serialization error and were not
-	 *                             successfully retried) +
+	 *                        successfully retried) +
 	 *   'deadlock_failures' (they got a deadlock error and were not
-	 *                        successfully retried).
+	 *                        successfully retried) +
+	 *   'other_sql_failures'  (they failed on the first try or after retries
+	 *                        due to a SQL error other than serialization or
+	 *                        deadlock; they are counted as a failed transaction
+	 *                        only when --continue-on-error is specified).
 	 *
 	 * If the transaction was retried after a serialization or a deadlock
 	 * error this does not guarantee that this retry was successful. Thus
@@ -421,7 +442,7 @@ typedef struct StatsData
 	 *
 	 * 'retried' (number of all retried transactions) =
 	 *   successfully retried transactions +
-	 *   failed transactions.
+	 *   unsuccessful retried transactions.
 	 *----------
 	 */
 	int64		cnt;			/* number of successful transactions, not
@@ -440,6 +461,11 @@ typedef struct StatsData
 	int64		deadlock_failures;	/* number of transactions that were not
 									 * successfully retried after a deadlock
 									 * error */
+	int64		other_sql_failures; /* number of failed transactions for
+									 * reasons other than
+									 * serialization/deadlock failure, which
+									 * is counted if --continue-on-error is
+									 * specified */
 	SimpleStats latency;
 	SimpleStats lag;
 } StatsData;
@@ -457,6 +483,7 @@ typedef enum EStatus
 {
 	ESTATUS_NO_ERROR = 0,
 	ESTATUS_META_COMMAND_ERROR,
+	ESTATUS_CONN_ERROR,
 
 	/* SQL errors */
 	ESTATUS_SERIALIZATION_ERROR,
@@ -770,6 +797,7 @@ static int64 total_weight = 0;
 static bool verbose_errors = false; /* print verbose messages of all errors */
 
 static bool exit_on_abort = false;	/* exit when any client is aborted */
+static bool continue_on_error = false;	/* continue after errors */
 
 /* Builtin test scripts */
 typedef struct BuiltinScript
@@ -842,7 +870,8 @@ static int	wait_on_socket_set(socket_set *sa, int64 usecs);
 static bool socket_has_input(socket_set *sa, int fd, int idx);
 
 /* callback used to build rows for COPY during data loading */
-typedef void (*initRowMethod) (PQExpBufferData *sql, int64 curr);
+typedef void (*initRowMethod)		(PQExpBufferData *sql, int64 curr);
+typedef void (*initRowMethodBin)	(PGconn *con, PGresult *res, int64_t curr, int32_t parent);
 
 /* callback functions for our flex lexer */
 static const PsqlScanCallbacks pgbench_callbacks = {
@@ -906,7 +935,10 @@ usage(void)
 		   "                           d: drop any existing pgbench tables\n"
 		   "                           t: create the tables used by the standard pgbench scenario\n"
 		   "                           g: generate data, client-side\n"
-		   "                           G: generate data, server-side\n"
+		   "                           C:   client-side (single TNX) COPY .. FROM STDIN .. BINARY\n"
+		   "                           G: generate data, server-side in single transaction\n"
+		   "                           i:   server-side (multiple TXNs) INSERT .. SELECT generate_series\n"
+		   "                           I:   server-side (multiple TXNs) INSERT .. SELECT unnest\n"
 		   "                           v: invoke VACUUM on the standard tables\n"
 		   "                           p: create primary key indexes on the standard tables\n"
 		   "                           f: create foreign keys between the standard tables\n"
@@ -949,6 +981,7 @@ usage(void)
 		   "  -T, --time=NUM           duration of benchmark test in seconds\n"
 		   "  -v, --vacuum-all         vacuum all four standard tables before tests\n"
 		   "  --aggregate-interval=NUM aggregate data over NUM seconds\n"
+		   "  --continue-on-error      continue running after an SQL error\n"
 		   "  --exit-on-abort          exit when any client is aborted\n"
 		   "  --failures-detailed      report the failures grouped by basic types\n"
 		   "  --log-prefix=PREFIX      prefix for transaction time log file\n"
@@ -1467,6 +1500,7 @@ initStats(StatsData *sd, pg_time_usec_t start)
 	sd->retried = 0;
 	sd->serialization_failures = 0;
 	sd->deadlock_failures = 0;
+	sd->other_sql_failures = 0;
 	initSimpleStats(&sd->latency);
 	initSimpleStats(&sd->lag);
 }
@@ -1516,6 +1550,9 @@ accumStats(StatsData *stats, bool skipped, double lat, double lag,
 		case ESTATUS_DEADLOCK_ERROR:
 			stats->deadlock_failures++;
 			break;
+		case ESTATUS_OTHER_SQL_ERROR:
+			stats->other_sql_failures++;
+			break;
 		default:
 			/* internal error which should never occur */
 			pg_fatal("unexpected error status: %d", estatus);
@@ -3231,11 +3268,43 @@ sendCommand(CState *st, Command *command)
 }
 
 /*
- * Get the error status from the error code.
+ * Read and discard all available results from the connection.
+ */
+static void
+discardAvailableResults(CState *st)
+{
+	PGresult   *res = NULL;
+
+	for (;;)
+	{
+		res = PQgetResult(st->con);
+
+		/*
+		 * Read and discard results until PQgetResult() returns NULL (no more
+		 * results) or a connection failure is detected. If the pipeline
+		 * status is PQ_PIPELINE_ABORTED, more results may still be available
+		 * even after PQgetResult() returns NULL, so continue reading in that
+		 * case.
+		 */
+		if ((res == NULL && PQpipelineStatus(st->con) != PQ_PIPELINE_ABORTED) ||
+			PQstatus(st->con) == CONNECTION_BAD)
+			break;
+
+		PQclear(res);
+	}
+	PQclear(res);
+}
+
+/*
+ * Determine the error status based on the connection status and error code.
  */
 static EStatus
-getSQLErrorStatus(const char *sqlState)
+getSQLErrorStatus(CState *st, const char *sqlState)
 {
+	discardAvailableResults(st);
+	if (PQstatus(st->con) == CONNECTION_BAD)
+		return ESTATUS_CONN_ERROR;
+
 	if (sqlState != NULL)
 	{
 		if (strcmp(sqlState, ERRCODE_T_R_SERIALIZATION_FAILURE) == 0)
@@ -3257,6 +3326,17 @@ canRetryError(EStatus estatus)
 			estatus == ESTATUS_DEADLOCK_ERROR);
 }
 
+/*
+ * Returns true if --continue-on-error is specified and this error allows
+ * processing to continue.
+ */
+static bool
+canContinueOnError(EStatus estatus)
+{
+	return (continue_on_error &&
+			estatus == ESTATUS_OTHER_SQL_ERROR);
+}
+
 /*
  * Process query response from the backend.
  *
@@ -3375,9 +3455,9 @@ readCommandResponse(CState *st, MetaCommand meta, char *varprefix)
 
 			case PGRES_NONFATAL_ERROR:
 			case PGRES_FATAL_ERROR:
-				st->estatus = getSQLErrorStatus(PQresultErrorField(res,
-																   PG_DIAG_SQLSTATE));
-				if (canRetryError(st->estatus))
+				st->estatus = getSQLErrorStatus(st, PQresultErrorField(res,
+																	   PG_DIAG_SQLSTATE));
+				if (canRetryError(st->estatus) || canContinueOnError(st->estatus))
 				{
 					if (verbose_errors)
 						commandError(st, PQresultErrorMessage(res));
@@ -3409,11 +3489,7 @@ readCommandResponse(CState *st, MetaCommand meta, char *varprefix)
 error:
 	PQclear(res);
 	PQclear(next_res);
-	do
-	{
-		res = PQgetResult(st->con);
-		PQclear(res);
-	} while (res);
+	discardAvailableResults(st);
 
 	return false;
 }
@@ -3511,14 +3587,18 @@ doRetry(CState *st, pg_time_usec_t *now)
 }
 
 /*
- * Read results and discard it until a sync point.
+ * Read and discard results until the last sync point.
  */
 static int
 discardUntilSync(CState *st)
 {
 	bool		received_sync = false;
 
-	/* send a sync */
+	/*
+	 * Send a Sync message to ensure at least one PGRES_PIPELINE_SYNC is
+	 * received and to avoid an infinite loop, since all earlier ones may have
+	 * already been received.
+	 */
 	if (!PQpipelineSync(st->con))
 	{
 		pg_log_error("client %d aborted: failed to send a pipeline sync",
@@ -3526,29 +3606,42 @@ discardUntilSync(CState *st)
 		return 0;
 	}
 
-	/* receive PGRES_PIPELINE_SYNC and null following it */
+	/*
+	 * Continue reading results until the last sync point, i.e., until
+	 * reaching null just after PGRES_PIPELINE_SYNC.
+	 */
 	for (;;)
 	{
 		PGresult   *res = PQgetResult(st->con);
 
+		if (PQstatus(st->con) == CONNECTION_BAD)
+		{
+			pg_log_error("client %d aborted while rolling back the transaction after an error; perhaps the backend died while processing",
+						 st->id);
+			PQclear(res);
+			return 0;
+		}
+
 		if (PQresultStatus(res) == PGRES_PIPELINE_SYNC)
 			received_sync = true;
-		else if (received_sync)
+		else if (received_sync && res == NULL)
 		{
-			/*
-			 * PGRES_PIPELINE_SYNC must be followed by another
-			 * PGRES_PIPELINE_SYNC or NULL; otherwise, assert failure.
-			 */
-			Assert(res == NULL);
-
 			/*
 			 * Reset ongoing sync count to 0 since all PGRES_PIPELINE_SYNC
 			 * results have been discarded.
 			 */
 			st->num_syncs = 0;
-			PQclear(res);
 			break;
 		}
+		else
+		{
+			/*
+			 * If a PGRES_PIPELINE_SYNC is followed by something other than
+			 * PGRES_PIPELINE_SYNC or NULL, another PGRES_PIPELINE_SYNC will
+			 * appear later. Reset received_sync to false to wait for it.
+			 */
+			received_sync = false;
+		}
 		PQclear(res);
 	}
 
@@ -4041,7 +4134,7 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg)
 					if (PQpipelineStatus(st->con) != PQ_PIPELINE_ON)
 						st->state = CSTATE_END_COMMAND;
 				}
-				else if (canRetryError(st->estatus))
+				else if (canRetryError(st->estatus) || canContinueOnError(st->estatus))
 					st->state = CSTATE_ERROR;
 				else
 					st->state = CSTATE_ABORTED;
@@ -4562,7 +4655,8 @@ static int64
 getFailures(const StatsData *stats)
 {
 	return (stats->serialization_failures +
-			stats->deadlock_failures);
+			stats->deadlock_failures +
+			stats->other_sql_failures);
 }
 
 /*
@@ -4582,6 +4676,8 @@ getResultString(bool skipped, EStatus estatus)
 				return "serialization";
 			case ESTATUS_DEADLOCK_ERROR:
 				return "deadlock";
+			case ESTATUS_OTHER_SQL_ERROR:
+				return "other";
 			default:
 				/* internal error which should never occur */
 				pg_fatal("unexpected error status: %d", estatus);
@@ -4637,6 +4733,7 @@ doLog(TState *thread, CState *st,
 			int64		skipped = 0;
 			int64		serialization_failures = 0;
 			int64		deadlock_failures = 0;
+			int64		other_sql_failures = 0;
 			int64		retried = 0;
 			int64		retries = 0;
 
@@ -4677,10 +4774,12 @@ doLog(TState *thread, CState *st,
 			{
 				serialization_failures = agg->serialization_failures;
 				deadlock_failures = agg->deadlock_failures;
+				other_sql_failures = agg->other_sql_failures;
 			}
-			fprintf(logfile, " " INT64_FORMAT " " INT64_FORMAT,
+			fprintf(logfile, " " INT64_FORMAT " " INT64_FORMAT " " INT64_FORMAT,
 					serialization_failures,
-					deadlock_failures);
+					deadlock_failures,
+					other_sql_failures);
 
 			fputc('\n', logfile);
 
@@ -4886,26 +4985,26 @@ initCreateTables(PGconn *con)
 	static const struct ddlinfo DDLs[] = {
 		{
 			"pgbench_history",
-			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22)",
-			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22)",
+			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22) default ''",
+			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22) default ''",
 			0
 		},
 		{
 			"pgbench_tellers",
-			"tid int not null,bid int,tbalance int,filler char(84)",
-			"tid int not null,bid int,tbalance int,filler char(84)",
+			"tid int not null,bid int,tbalance int,filler char(84) default ''",
+			"tid int not null,bid int,tbalance int,filler char(84) default ''",
 			1
 		},
 		{
 			"pgbench_accounts",
-			"aid    int not null,bid int,abalance int,filler char(84)",
-			"aid bigint not null,bid int,abalance int,filler char(84)",
+			"aid    int not null,bid int,abalance int,filler char(84) default ''",
+			"aid bigint not null,bid int,abalance int,filler char(84) default ''",
 			1
 		},
 		{
 			"pgbench_branches",
-			"bid int not null,bbalance int,filler char(88)",
-			"bid int not null,bbalance int,filler char(88)",
+			"bid int not null,bbalance int,filler char(88) default ''",
+			"bid int not null,bbalance int,filler char(88) default ''",
 			1
 		}
 	};
@@ -5120,9 +5219,9 @@ initPopulateTable(PGconn *con, const char *table, int64 base,
  * a blank-padded string in pgbench_accounts.
  */
 static void
-initGenerateDataClientSide(PGconn *con)
+initGenerateDataClientSideText(PGconn *con)
 {
-	fprintf(stderr, "generating data (client-side)...\n");
+	fprintf(stderr, "TEXT mode...\n");
 
 	/*
 	 * we do all of this in one transaction to enable the backend's
@@ -5138,25 +5237,389 @@ initGenerateDataClientSide(PGconn *con)
 	 * already exist
 	 */
 	initPopulateTable(con, "pgbench_branches", nbranches, initBranch);
-	initPopulateTable(con, "pgbench_tellers", ntellers, initTeller);
+	initPopulateTable(con, "pgbench_tellers",  ntellers,  initTeller);
 	initPopulateTable(con, "pgbench_accounts", naccounts, initAccount);
 
 	executeStatement(con, "commit");
 }
 
+
 /*
- * Fill the standard tables with some data generated on the server
- *
- * As already the case with the client-side data generation, the filler
- * column defaults to NULL in pgbench_branches and pgbench_tellers,
- * and is a blank-padded string in pgbench_accounts.
+ * Dumps binary buffer to file (purely for debugging)
  */
 static void
-initGenerateDataServerSide(PGconn *con)
+dumpBufferToFile(char *filename)
+{
+	FILE *file_ptr;
+	size_t bytes_written;
+
+	file_ptr = fopen(filename, "wb");
+	if (file_ptr == NULL)
+	{
+		fprintf(stderr, "Error opening file %s\n", filename);
+		return; // EXIT_FAILURE;
+	}
+
+	bytes_written = fwrite(bin_copy_buffer, 1, bin_copy_buffer_length, file_ptr);
+
+	if (bytes_written != bin_copy_buffer_length)
+	{
+		fprintf(stderr, "Error writing to file or incomplete write\n");
+		fclose(file_ptr);
+		return; // EXIT_FAILURE;
+	}
+
+	fclose(file_ptr);
+}
+
+/*
+ * Save char data to buffer
+ */
+static void
+bufferCharData(char *src, int32_t len)
+{
+	memcpy((char *) bin_copy_buffer + bin_copy_buffer_length, (char *) src, len);
+	bin_copy_buffer_length += len;
+}
+
+/*
+ * Converts platform byte order into network byte order
+ * SPARC doesn't reqire that
+ */
+static void
+bufferData(void *src, int32_t len)
+{
+#ifdef __sparc__
+	bufferCharData(src, len);
+#else
+	if (len == 1)
+		bufferCharData(src, len);
+	else
+		for (int32_t i = 0; i < len; i++)
+		{
+			((char *) bin_copy_buffer + bin_copy_buffer_length)[i] =
+				((char *) src)[len - i - 1];
+		}
+
+	bin_copy_buffer_length += len;
+#endif
+}
+
+/*
+ * adds column counter
+ */
+static void
+addColumnCounter(int16_t n)
+{
+	bufferData((void *) &n, sizeof(n));
+}
+
+/*
+ * adds column with NULL value
+ */
+static void
+addNullColumn()
+{
+	int32_t null = -1;
+	bufferData((void *) &null, sizeof(null));
+}
+
+/*
+ * adds column with int8 value
+ */
+static void
+addInt8Column(int8_t value)
+{
+	int8_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with int16 value
+ */
+static void
+addInt16Column(int16_t value)
+{
+	int16_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with inti32 value
+ */
+static void
+addInt32Column(int32_t value)
+{
+	int32_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with inti64 value
+ */
+static void
+addInt64Column(int64_t value)
+{
+	int64_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with char value
+ */
+static void
+addCharColumn(char *value)
+{
+	int32_t	size = strlen(value);
+	bufferData((void *) &size, sizeof(size));
+	bufferCharData(value, size);
+}
+
+/*
+ * Starts communication with server for COPY FROM BINARY statement
+ */
+static void
+sendBinaryCopyHeader(PGconn *con)
+{
+	char header[] = {'P','G','C','O','P','Y','\n','\377','\r','\n','\0',
+					 '\0','\0','\0','\0',
+					 '\0','\0','\0','\0' };
+
+	PQputCopyData(con, header, 19);
+}
+
+/*
+ * Finishes communication with server for COPY FROM BINARY statement
+ */
+static void
+sendBinaryCopyTrailer(PGconn *con)
+{
+	static char trailer[] = { 0xFF, 0xFF };
+
+	PQputCopyData(con, trailer, 2);
+}
+
+/*
+ * Flashes current buffer over network if needed
+ */
+static void
+flushBuffer(PGconn *con, PGresult *res, int16_t row_len)
+{
+	if (bin_copy_buffer_length + row_len > BIN_COPY_BUF_SIZE)
+	{
+		/* flush current buffer */
+		if (PQresultStatus(res) == PGRES_COPY_IN)
+			PQputCopyData(con, (char *) bin_copy_buffer, bin_copy_buffer_length);
+		bin_copy_buffer_length = 0;
+	}
+}
+
+/*
+ * Sends current branch row to buffer
+ */
+static void
+initBranchBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len =  35 + 2 + 4*3; /* max row size is 32 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(2);
+
+	addInt32Column(curr + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Sends current teller row to buffer
+ */
+static void
+initTellerBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len =  40 + 2 + 4*4; /* max row size is 40 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(3);
+
+	addInt32Column(curr + 1);
+	addInt32Column(curr / parent + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Sends current account row to buffer
+ */
+static void
+initAccountBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len = 250 + 2 + 4*4; /* max row size is 250 for int64 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(3);
+
+	if (scale <= SCALE_32BIT_THRESHOLD)
+		addInt32Column(curr + 1);
+	else
+		addInt64Column(curr);
+
+	addInt32Column(curr / parent + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Universal wrapper for sending data in binary format
+ */
+static void
+initPopulateTableBinary(PGconn *con, char *table, char *columns,
+						int counter, int64_t base, initRowMethodBin init_row)
+{
+	int			 n;
+	PGresult	*res;
+	char		 copy_statement[256];
+	const char	*copy_statement_fmt = "copy %s (%s) from stdin (format binary)";
+	int64_t		 start = base * counter;
+
+	bin_copy_buffer_length = 0;
+
+	/* Use COPY with FREEZE on v14 and later for all ordinary tables */
+	if ((PQserverVersion(con) >= 140000) &&
+		get_table_relkind(con, table) == RELKIND_RELATION)
+		copy_statement_fmt = "copy %s (%s) from stdin with (format binary)";
+
+	n = pg_snprintf(copy_statement, sizeof(copy_statement), copy_statement_fmt, table, columns);
+	if (n >= sizeof(copy_statement))
+		pg_fatal("invalid buffer size: must be at least %d characters long", n);
+	else if (n == -1)
+		pg_fatal("invalid format string");
+
+	res = PQexec(con, copy_statement);
+
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("unexpected copy in result: %s", PQerrorMessage(con));
+	PQclear(res);
+
+
+	sendBinaryCopyHeader(con);
+
+	for (int64_t i = start; i < start + base; i++)
+	{
+		init_row(con, res, i, base);
+	}
+
+	if (PQresultStatus(res) == PGRES_COPY_IN)
+		PQputCopyData(con, (char *) bin_copy_buffer, bin_copy_buffer_length);
+	else
+		fprintf(stderr, "Unexpected mode %d instead of %d\n", PQresultStatus(res), PGRES_COPY_IN);
+
+	sendBinaryCopyTrailer(con);
+
+	if (PQresultStatus(res) == PGRES_COPY_IN)
+	{
+		if (PQputCopyEnd(con, NULL) == 1) /* success */
+		{
+			res = PQgetResult(con);
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				fprintf(stderr, "Error: %s\n", PQerrorMessage(con));
+			PQclear(res);
+		}
+		else
+			fprintf(stderr, "Error: %s\n", PQerrorMessage(con));
+	}
+}
+
+/*
+ * Wrapper for binary data load
+ */
+static void
+initGenerateDataClientSideBinary(PGconn *con)
+{
+
+	fprintf(stderr, "BINARY mode...\n");
+
+	bin_copy_buffer = pg_malloc(BIN_COPY_BUF_SIZE);
+	bin_copy_buffer_length = 0;
+
+	/*
+	 * we do all of this in one transaction to enable the backend's
+	 * data-loading optimizations
+	 */
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	executeStatement(con, "commit");
+
+	for (int i = 0; i < scale; i++)
+	{
+		initPopulateTableBinary(con, "pgbench_branches", "bid, bbalance",
+								i, nbranches, initBranchBinary);
+		initPopulateTableBinary(con, "pgbench_tellers",  "tid, bid, tbalance",
+								i, ntellers,  initTellerBinary);
+		initPopulateTableBinary(con, "pgbench_accounts", "aid, bid, abalance",
+								i, naccounts, initAccountBinary);
+
+		executeStatement(con, "commit");
+	}
+
+	pg_free(bin_copy_buffer);
+}
+
+/*
+ * Fill the standard tables with some data generated and sent from the client.
+ */
+static void
+initGenerateDataClientSide(PGconn *con)
+{
+	fprintf(stderr, "generating data (client-side) in ");
+
+	switch (data_generation_type)
+	{
+		case GEN_TYPE_COPY_ORIGINAL:
+			initGenerateDataClientSideText(con);
+			break;
+		case GEN_TYPE_COPY_BINARY:
+			initGenerateDataClientSideBinary(con);
+			break;
+	}
+}
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM generate_series
+ * whole dataset in single transaction
+ */
+static void
+generateDataInsertSingleTXN(PGconn *con)
 {
 	PQExpBufferData sql;
 
-	fprintf(stderr, "generating data (server-side)...\n");
+	fprintf(stderr, "via INSERT .. SELECT generate_series... in single TXN\n");
+
 
 	/*
 	 * we do all of this in one transaction to enable the backend's
@@ -5170,27 +5633,166 @@ initGenerateDataServerSide(PGconn *con)
 	initPQExpBuffer(&sql);
 
 	printfPQExpBuffer(&sql,
-					  "insert into pgbench_branches(bid,bbalance) "
+					  "insert into pgbench_branches(bid, bbalance) "
 					  "select bid, 0 "
-					  "from generate_series(1, %d) as bid", nbranches * scale);
+					  "from generate_series(1, %d) as bid",
+					  scale * nbranches);
 	executeStatement(con, sql.data);
 
 	printfPQExpBuffer(&sql,
-					  "insert into pgbench_tellers(tid,bid,tbalance) "
-					  "select tid, (tid - 1) / %d + 1, 0 "
-					  "from generate_series(1, %d) as tid", ntellers, ntellers * scale);
+					  "insert into pgbench_tellers(tid, bid, tbalance) "
+					  "select tid + 1, tid / %d + 1, 0 "
+					  "from generate_series(0, %d) as tid",
+					  ntellers, (scale * ntellers) - 1);
 	executeStatement(con, sql.data);
 
 	printfPQExpBuffer(&sql,
-					  "insert into pgbench_accounts(aid,bid,abalance,filler) "
-					  "select aid, (aid - 1) / %d + 1, 0, '' "
-					  "from generate_series(1, " INT64_FORMAT ") as aid",
-					  naccounts, (int64) naccounts * scale);
+					  "insert into pgbench_accounts(aid, bid, abalance, "
+								   "filler) "
+					  "select aid + 1, aid / %d + 1, 0, '' "
+					  "from generate_series(0, " INT64_FORMAT ") as aid",
+					  naccounts, (int64) (scale * naccounts) - 1);
 	executeStatement(con, sql.data);
 
+	executeStatement(con, "commit");
+
 	termPQExpBuffer(&sql);
+}
+
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM generate_series
+ * One transaction per 'scale'
+ */
+static void
+generateDataInsertSeries(PGconn *con)
+{
+	PQExpBufferData sql;
+
+	fprintf(stderr, "via INSERT .. SELECT generate_series... in multiple TXN(s)\n");
+
+	initPQExpBuffer(&sql);
+
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
 
 	executeStatement(con, "commit");
+
+	for (int i = 0; i < scale; i++)
+	{
+		executeStatement(con, "begin");
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_branches(bid, bbalance) "
+						  "values(%d, 0)", i + 1);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_tellers(tid, bid, tbalance) "
+						  "select tid + 1, tid / %d + 1, 0 "
+						  "from generate_series(%d, %d) as tid",
+						  ntellers, i * ntellers, (i + 1) * ntellers - 1);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_accounts(aid, bid, abalance, "
+									   "filler) "
+						  "select aid + 1, aid / %d + 1, 0, '' "
+						  "from generate_series(" INT64_FORMAT ", "
+								INT64_FORMAT ") as aid",
+						  naccounts, (int64) i * naccounts,
+						  (int64) (i + 1) * naccounts - 1);
+		executeStatement(con, sql.data);
+
+		executeStatement(con, "commit");
+	}
+
+	termPQExpBuffer(&sql);
+}
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM unnest
+ * One transaction per 'scale'
+ */
+static void
+generateDataInsertUnnest(PGconn *con)
+{
+	PQExpBufferData sql;
+
+	fprintf(stderr, "via INSERT .. SELECT unnest...\n");
+
+	initPQExpBuffer(&sql);
+
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	executeStatement(con, "commit");
+
+	for (int s = 0; s < scale; s++)
+	{
+		executeStatement(con, "begin");
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_branches(bid,bbalance) "
+						  "values(%d, 0)", s + 1);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_tellers(tid, bid, tbalance) "
+						  "select unnest(array_agg(s.i order by s.i)) as tid, "
+								  "%d as bid, 0 as tbalance "
+						  "from generate_series(%d, %d) as s(i)",
+						  s + 1, s * ntellers + 1, (s + 1) * ntellers);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "with data as ("
+						  "   select generate_series(" INT64_FORMAT ", "
+							  INT64_FORMAT ") as i) "
+						  "insert into pgbench_accounts(aid, bid, "
+									  "abalance, filler) "
+						  "select unnest(aid), unnest(bid), 0 as abalance, "
+								  "'' as filler "
+						  "from (select array_agg(i+1) aid, "
+									   "array_agg(i/%d + 1) bid from data)",
+						  (int64) s * naccounts + 1,
+						  (int64) (s + 1) * naccounts, naccounts);
+		executeStatement(con, sql.data);
+
+		executeStatement(con, "commit");
+	}
+
+	termPQExpBuffer(&sql);
+}
+
+/*
+ * Fill the standard tables with some data generated on the server
+ *
+ * As already the case with the client-side data generation, the filler
+ * column defaults to NULL in pgbench_branches and pgbench_tellers,
+ * and is a blank-padded string in pgbench_accounts.
+ */
+static void
+initGenerateDataServerSide(PGconn *con)
+{
+	fprintf(stderr, "generating data (server-side) ");
+
+	switch (data_generation_type)
+	{
+		case GEN_TYPE_INSERT_ORIGINAL:
+			generateDataInsertSingleTXN(con);
+			break;
+		case GEN_TYPE_INSERT_SERIES:
+			generateDataInsertSeries(con);
+			break;
+		case GEN_TYPE_INSERT_UNNEST:
+			generateDataInsertUnnest(con);
+			break;
+	}
 }
 
 /*
@@ -5276,6 +5878,8 @@ initCreateFKeys(PGconn *con)
 static void
 checkInitSteps(const char *initialize_steps)
 {
+	char	data_init_type = 0;
+
 	if (initialize_steps[0] == '\0')
 		pg_fatal("no initialization steps specified");
 
@@ -5287,7 +5891,22 @@ checkInitSteps(const char *initialize_steps)
 			pg_log_error_detail("Allowed step characters are: \"" ALL_INIT_STEPS "\".");
 			exit(1);
 		}
+
+		switch (*step)
+		{
+			case 'g':
+			case 'C':
+			case 'G':
+			case 'i':
+			case 'I':
+				data_init_type++;
+				data_generation_type = *step;
+				break;
+		}
 	}
+
+	if (data_init_type > 1)
+		pg_log_error("WARNING! More than one type of server-side data generation is requested");
 }
 
 /*
@@ -5326,10 +5945,13 @@ runInitSteps(const char *initialize_steps)
 				initCreateTables(con);
 				break;
 			case 'g':
+			case 'C':
 				op = "client-side generate";
 				initGenerateDataClientSide(con);
 				break;
 			case 'G':
+			case 'i':
+			case 'I':
 				op = "server-side generate";
 				initGenerateDataServerSide(con);
 				break;
@@ -6319,6 +6941,7 @@ printProgressReport(TState *threads, int64 test_start, pg_time_usec_t now,
 		cur.serialization_failures +=
 			threads[i].stats.serialization_failures;
 		cur.deadlock_failures += threads[i].stats.deadlock_failures;
+		cur.other_sql_failures += threads[i].stats.other_sql_failures;
 	}
 
 	/* we count only actually executed transactions */
@@ -6461,7 +7084,8 @@ printResults(StatsData *total,
 
 	/*
 	 * Remaining stats are nonsensical if we failed to execute any xacts due
-	 * to others than serialization or deadlock errors
+	 * to other than serialization or deadlock errors and --continue-on-error
+	 * is not set.
 	 */
 	if (total_cnt <= 0)
 		return;
@@ -6477,6 +7101,9 @@ printResults(StatsData *total,
 		printf("number of deadlock failures: " INT64_FORMAT " (%.3f%%)\n",
 			   total->deadlock_failures,
 			   100.0 * total->deadlock_failures / total_cnt);
+		printf("number of other failures: " INT64_FORMAT " (%.3f%%)\n",
+			   total->other_sql_failures,
+			   100.0 * total->other_sql_failures / total_cnt);
 	}
 
 	/* it can be non-zero only if max_tries is not equal to one */
@@ -6580,6 +7207,10 @@ printResults(StatsData *total,
 							   sstats->deadlock_failures,
 							   (100.0 * sstats->deadlock_failures /
 								script_total_cnt));
+						printf(" - number of other failures: " INT64_FORMAT " (%.3f%%)\n",
+							   sstats->other_sql_failures,
+							   (100.0 * sstats->other_sql_failures /
+								script_total_cnt));
 					}
 
 					/*
@@ -6739,6 +7370,7 @@ main(int argc, char **argv)
 		{"verbose-errors", no_argument, NULL, 15},
 		{"exit-on-abort", no_argument, NULL, 16},
 		{"debug", no_argument, NULL, 17},
+		{"continue-on-error", no_argument, NULL, 18},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -7092,6 +7724,10 @@ main(int argc, char **argv)
 			case 17:			/* debug */
 				pg_logging_increase_verbosity();
 				break;
+			case 18:			/* continue-on-error */
+				benchmarking_option_set = true;
+				continue_on_error = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -7447,6 +8083,7 @@ main(int argc, char **argv)
 		stats.retried += thread->stats.retried;
 		stats.serialization_failures += thread->stats.serialization_failures;
 		stats.deadlock_failures += thread->stats.deadlock_failures;
+		stats.other_sql_failures += thread->stats.other_sql_failures;
 		latency_late += thread->latency_late;
 		conn_total_duration += thread->conn_duration;
 
#7Boris Mironov
boris_mironov@outlook.com
In reply to: Boris Mironov (#6)
1 attachment(s)
Re: Idea to enhance pgbench by more modes to generate data (multi-TXNs, UNNEST, COPY BINARY)

Hello,

Adding tests for new modes into Perl testing framework for pgbench.

The goal is to pass GitHub checks for the patch in green to simplify reviewer's life.

Cheers,
Boris

Attachments:

v3-pgbench-faster-modes.patchapplication/octet-stream; name=v3-pgbench-faster-modes.patchDownload
From c768f399c556295de7d53895410e686d86b4b960 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Sun, 9 Nov 2025 19:34:58 +0700
Subject: [PATCH 01/10] Converting one huge transaction into series of one per
 'scale'

---
 src/bin/pgbench/pgbench.c | 61 ++++++++++++++++++++++++++-------------
 1 file changed, 41 insertions(+), 20 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index d8764ba6fe0..284a7c860f1 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -181,6 +181,12 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
  */
 static int	scale = 1;
 
+/*
+ * scaling factor after which we switch to multiple transactions during
+ * data population phase on server side
+ */
+static int64	single_txn_scale_limit = 1;
+
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -5213,6 +5219,7 @@ static void
 initGenerateDataServerSide(PGconn *con)
 {
 	PQExpBufferData sql;
+	int				chunk = (scale >= single_txn_scale_limit) ? 1 : scale;
 
 	fprintf(stderr, "generating data (server-side)...\n");
 
@@ -5225,30 +5232,44 @@ initGenerateDataServerSide(PGconn *con)
 	/* truncate away any old data */
 	initTruncateTables(con);
 
+	executeStatement(con, "commit");
+
 	initPQExpBuffer(&sql);
 
-	printfPQExpBuffer(&sql,
-					  "insert into pgbench_branches(bid,bbalance) "
-					  "select bid, 0 "
-					  "from generate_series(1, %d) as bid", nbranches * scale);
-	executeStatement(con, sql.data);
-
-	printfPQExpBuffer(&sql,
-					  "insert into pgbench_tellers(tid,bid,tbalance) "
-					  "select tid, (tid - 1) / %d + 1, 0 "
-					  "from generate_series(1, %d) as tid", ntellers, ntellers * scale);
-	executeStatement(con, sql.data);
-
-	printfPQExpBuffer(&sql,
-					  "insert into pgbench_accounts(aid,bid,abalance,filler) "
-					  "select aid, (aid - 1) / %d + 1, 0, '' "
-					  "from generate_series(1, " INT64_FORMAT ") as aid",
-					  naccounts, (int64) naccounts * scale);
-	executeStatement(con, sql.data);
+	for (int i = 0; i < scale; i += chunk) {
+		executeStatement(con, "begin");
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_branches(bid,bbalance) "
+						  "select bid + 1, 0 "
+						  "from generate_series(%d, %d) as bid", i, i + chunk);
+						  //"select bid, 0 "
+						  //"from generate_series(1, %d) as bid", nbranches * scale);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_tellers(tid,bid,tbalance) "
+						  "select tid + 1, tid / %d + 1, 0 "
+						  "from generate_series(%d, %d) as tid",
+						  ntellers, i * ntellers, (i + chunk) * ntellers - 1);
+						  //"select tid, (tid - 1) / %d + 1, 0 "
+						  //"from generate_series(1, %d) as tid", ntellers, ntellers * scale);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_accounts(aid,bid,abalance,filler) "
+						  "select aid + 1, aid / %d + 1, 0, '' "
+						  "from generate_series(" INT64_FORMAT ", " INT64_FORMAT ") as aid",
+						  naccounts, (int64) i * naccounts, (int64) (i + chunk) * naccounts - 1);
+						  //"select aid, (aid - 1) / %d + 1, 0, '' "
+						  //"from generate_series(1, " INT64_FORMAT ") as aid",
+						  //naccounts, (int64) naccounts * scale);
+		executeStatement(con, sql.data);
+
+		executeStatement(con, "commit");
+	}
 
 	termPQExpBuffer(&sql);
-
-	executeStatement(con, "commit");
 }
 
 /*
-- 
2.43.0


From 0eddb156c187d829c4381bc928c5314705928852 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Sun, 9 Nov 2025 20:13:23 +0700
Subject: [PATCH 02/10] Getting rid off limit for single transaction size
 during data generation

---
 src/bin/pgbench/pgbench.c | 15 ++++-----------
 1 file changed, 4 insertions(+), 11 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 284a7c860f1..28b72e4cf1f 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -181,12 +181,6 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
  */
 static int	scale = 1;
 
-/*
- * scaling factor after which we switch to multiple transactions during
- * data population phase on server side
- */
-static int64	single_txn_scale_limit = 1;
-
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -5219,7 +5213,6 @@ static void
 initGenerateDataServerSide(PGconn *con)
 {
 	PQExpBufferData sql;
-	int				chunk = (scale >= single_txn_scale_limit) ? 1 : scale;
 
 	fprintf(stderr, "generating data (server-side)...\n");
 
@@ -5236,13 +5229,13 @@ initGenerateDataServerSide(PGconn *con)
 
 	initPQExpBuffer(&sql);
 
-	for (int i = 0; i < scale; i += chunk) {
+	for (int i = 0; i < scale; i++) {
 		executeStatement(con, "begin");
 
 		printfPQExpBuffer(&sql,
 						  "insert into pgbench_branches(bid,bbalance) "
 						  "select bid + 1, 0 "
-						  "from generate_series(%d, %d) as bid", i, i + chunk);
+						  "from generate_series(%d, %d) as bid", i, i + 1);
 						  //"select bid, 0 "
 						  //"from generate_series(1, %d) as bid", nbranches * scale);
 		executeStatement(con, sql.data);
@@ -5251,7 +5244,7 @@ initGenerateDataServerSide(PGconn *con)
 						  "insert into pgbench_tellers(tid,bid,tbalance) "
 						  "select tid + 1, tid / %d + 1, 0 "
 						  "from generate_series(%d, %d) as tid",
-						  ntellers, i * ntellers, (i + chunk) * ntellers - 1);
+						  ntellers, i * ntellers, (i + 1) * ntellers - 1);
 						  //"select tid, (tid - 1) / %d + 1, 0 "
 						  //"from generate_series(1, %d) as tid", ntellers, ntellers * scale);
 		executeStatement(con, sql.data);
@@ -5260,7 +5253,7 @@ initGenerateDataServerSide(PGconn *con)
 						  "insert into pgbench_accounts(aid,bid,abalance,filler) "
 						  "select aid + 1, aid / %d + 1, 0, '' "
 						  "from generate_series(" INT64_FORMAT ", " INT64_FORMAT ") as aid",
-						  naccounts, (int64) i * naccounts, (int64) (i + chunk) * naccounts - 1);
+						  naccounts, (int64) i * naccounts, (int64) (i + 1) * naccounts - 1);
 						  //"select aid, (aid - 1) / %d + 1, 0, '' "
 						  //"from generate_series(1, " INT64_FORMAT ") as aid",
 						  //naccounts, (int64) naccounts * scale);
-- 
2.43.0


From c5659cf474ec273c057668f30a4f435fd02f2da7 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Sun, 9 Nov 2025 20:38:36 +0700
Subject: [PATCH 03/10] No need to keep old code in comments

---
 src/bin/pgbench/pgbench.c | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 28b72e4cf1f..97895aa9edf 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -5236,8 +5236,6 @@ initGenerateDataServerSide(PGconn *con)
 						  "insert into pgbench_branches(bid,bbalance) "
 						  "select bid + 1, 0 "
 						  "from generate_series(%d, %d) as bid", i, i + 1);
-						  //"select bid, 0 "
-						  //"from generate_series(1, %d) as bid", nbranches * scale);
 		executeStatement(con, sql.data);
 
 		printfPQExpBuffer(&sql,
@@ -5245,8 +5243,6 @@ initGenerateDataServerSide(PGconn *con)
 						  "select tid + 1, tid / %d + 1, 0 "
 						  "from generate_series(%d, %d) as tid",
 						  ntellers, i * ntellers, (i + 1) * ntellers - 1);
-						  //"select tid, (tid - 1) / %d + 1, 0 "
-						  //"from generate_series(1, %d) as tid", ntellers, ntellers * scale);
 		executeStatement(con, sql.data);
 
 		printfPQExpBuffer(&sql,
@@ -5254,9 +5250,6 @@ initGenerateDataServerSide(PGconn *con)
 						  "select aid + 1, aid / %d + 1, 0, '' "
 						  "from generate_series(" INT64_FORMAT ", " INT64_FORMAT ") as aid",
 						  naccounts, (int64) i * naccounts, (int64) (i + 1) * naccounts - 1);
-						  //"select aid, (aid - 1) / %d + 1, 0, '' "
-						  //"from generate_series(1, " INT64_FORMAT ") as aid",
-						  //naccounts, (int64) naccounts * scale);
 		executeStatement(con, sql.data);
 
 		executeStatement(con, "commit");
-- 
2.43.0


From e47b52ddf23593dad9375ef5356fd41d0621ede3 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Mon, 10 Nov 2025 19:06:48 +0700
Subject: [PATCH 04/10] Adding server-side data generation via unnest

---
 src/bin/pgbench/pgbench.c | 199 ++++++++++++++++++++++++++++++++++----
 1 file changed, 182 insertions(+), 17 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 97895aa9edf..65d77cdefea 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -161,7 +161,7 @@ typedef struct socket_set
  * some configurable parameters */
 
 #define DEFAULT_INIT_STEPS "dtgvp"	/* default -I setting */
-#define ALL_INIT_STEPS "dtgGvpf"	/* all possible steps */
+#define ALL_INIT_STEPS "dtgGiIvpf"	/* all possible steps */
 
 #define LOG_STEP_SECONDS	5	/* seconds between log messages */
 #define DEFAULT_NXACTS	10		/* default nxacts */
@@ -171,6 +171,12 @@ typedef struct socket_set
 #define MIN_ZIPFIAN_PARAM		1.001	/* minimum parameter for zipfian */
 #define MAX_ZIPFIAN_PARAM		1000.0	/* maximum parameter for zipfian */
 
+/* original single transaction server-side method */
+#define GEN_TYPE_INSERT_ORIGINAL	'G'	/* use INSERT .. SELECT generate_series to generate data */
+/* 'one transaction per scale' server-side methods */
+#define GEN_TYPE_INSERT_SERIES		'i'	/* use INSERT .. SELECT generate_series to generate data */
+#define GEN_TYPE_INSERT_UNNEST  	'I'	/* use INSERT .. SELECT unnest to generate data */
+
 static int	nxacts = 0;			/* number of transactions per client */
 static int	duration = 0;		/* duration in seconds */
 static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
@@ -181,6 +187,11 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
  */
 static int	scale = 1;
 
+/*
+ *
+ */
+static char	data_generation_type = '?';
+
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -914,7 +925,9 @@ usage(void)
 		   "                           d: drop any existing pgbench tables\n"
 		   "                           t: create the tables used by the standard pgbench scenario\n"
 		   "                           g: generate data, client-side\n"
-		   "                           G: generate data, server-side\n"
+		   "                           G: generate data, server-side in single transaction\n"
+		   "                           i:   server-side (multiple TXNs) INSERT .. SELECT generate_series\n"
+		   "                           I:   server-side (multiple TXNs) INSERT .. SELECT unnest\n"
 		   "                           v: invoke VACUUM on the standard tables\n"
 		   "                           p: create primary key indexes on the standard tables\n"
 		   "                           f: create foreign keys between the standard tables\n"
@@ -5203,18 +5216,16 @@ initGenerateDataClientSide(PGconn *con)
 }
 
 /*
- * Fill the standard tables with some data generated on the server
- *
- * As already the case with the client-side data generation, the filler
- * column defaults to NULL in pgbench_branches and pgbench_tellers,
- * and is a blank-padded string in pgbench_accounts.
+ * Generating data via INSERT .. SELECT .. FROM generate_series
+ * whole dataset in single transaction
  */
 static void
-initGenerateDataServerSide(PGconn *con)
+generateDataInsertSingleTXN(PGconn *con)
 {
 	PQExpBufferData sql;
 
-	fprintf(stderr, "generating data (server-side)...\n");
+	fprintf(stderr, "via INSERT .. SELECT generate_series... in single TXN\n");
+
 
 	/*
 	 * we do all of this in one transaction to enable the backend's
@@ -5225,31 +5236,136 @@ initGenerateDataServerSide(PGconn *con)
 	/* truncate away any old data */
 	initTruncateTables(con);
 
+	initPQExpBuffer(&sql);
+
+	printfPQExpBuffer(&sql,
+					  "insert into pgbench_branches(bid, bbalance) "
+					  "select bid, 0 "
+					  "from generate_series(1, %d)", scale * nbranches);
+	executeStatement(con, sql.data);
+
+	printfPQExpBuffer(&sql,
+					  "insert into pgbench_tellers(tid, bid, tbalance) "
+					  "select tid + 1, tid / %d + 1, 0 "
+					  "from generate_series(0, %d) as tid",
+					  ntellers, (scale * ntellers) - 1);
+	executeStatement(con, sql.data);
+
+	printfPQExpBuffer(&sql,
+					  "insert into pgbench_accounts(aid, bid, abalance, "
+								   "filler) "
+					  "select aid + 1, aid / %d + 1, 0, '' "
+					  "from generate_series(0, " INT64_FORMAT ") as aid",
+					  naccounts, (int64) (scale * naccounts) - 1);
+	executeStatement(con, sql.data);
+
 	executeStatement(con, "commit");
 
+	termPQExpBuffer(&sql);
+}
+
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM generate_series
+ * One transaction per 'scale'
+ */
+static void
+generateDataInsertSeries(PGconn *con)
+{
+	PQExpBufferData sql;
+
+	fprintf(stderr, "via INSERT .. SELECT generate_series... in multiple TXN(s)\n");
+
 	initPQExpBuffer(&sql);
 
-	for (int i = 0; i < scale; i++) {
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	executeStatement(con, "commit");
+
+	for (int i = 0; i < scale; i++)
+	{
 		executeStatement(con, "begin");
 
 		printfPQExpBuffer(&sql,
-						  "insert into pgbench_branches(bid,bbalance) "
-						  "select bid + 1, 0 "
-						  "from generate_series(%d, %d) as bid", i, i + 1);
+						  "insert into pgbench_branches(bid, bbalance) "
+						  "values(%d, 0)", i + 1);
 		executeStatement(con, sql.data);
 
 		printfPQExpBuffer(&sql,
-						  "insert into pgbench_tellers(tid,bid,tbalance) "
+						  "insert into pgbench_tellers(tid, bid, tbalance) "
 						  "select tid + 1, tid / %d + 1, 0 "
 						  "from generate_series(%d, %d) as tid",
 						  ntellers, i * ntellers, (i + 1) * ntellers - 1);
 		executeStatement(con, sql.data);
 
 		printfPQExpBuffer(&sql,
-						  "insert into pgbench_accounts(aid,bid,abalance,filler) "
+						  "insert into pgbench_accounts(aid, bid, abalance, "
+									   "filler) "
 						  "select aid + 1, aid / %d + 1, 0, '' "
-						  "from generate_series(" INT64_FORMAT ", " INT64_FORMAT ") as aid",
-						  naccounts, (int64) i * naccounts, (int64) (i + 1) * naccounts - 1);
+						  "from generate_series(" INT64_FORMAT ", "
+								INT64_FORMAT ") as aid",
+						  naccounts, (int64) i * naccounts,
+						  (int64) (i + 1) * naccounts - 1);
+		executeStatement(con, sql.data);
+
+		executeStatement(con, "commit");
+	}
+
+	termPQExpBuffer(&sql);
+}
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM unnest
+ * One transaction per 'scale'
+ */
+static void
+generateDataInsertUnnest(PGconn *con)
+{
+	PQExpBufferData sql;
+
+	fprintf(stderr, "via INSERT .. SELECT unnest...\n");
+
+	initPQExpBuffer(&sql);
+
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	executeStatement(con, "commit");
+
+	for (int s = 0; s < scale; s++)
+	{
+		executeStatement(con, "begin");
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_branches(bid,bbalance) "
+						  "values(%d, 0)", s + 1);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_tellers(tid, bid, tbalance) "
+						  "select unnest(array_agg(s.i order by s.i)) as tid, "
+								  "%d as bid, 0 as tbalance "
+						  "from generate_series(%d, %d) as s(i)",
+						  s + 1, s * ntellers + 1, (s + 1) * ntellers);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "with data as ("
+						  "   select generate_series(" INT64_FORMAT ", "
+							  INT64_FORMAT ") as i) "
+						  "insert into pgbench_accounts(aid, bid, "
+									  "abalance, filler) "
+						  "select unnest(aid), unnest(bid), 0 as abalance, "
+								  "'' as filler "
+						  "from (select array_agg(i+1) aid, "
+									   "array_agg(i/%d + 1) bid from data)",
+						  (int64) s * naccounts + 1,
+						  (int64) (s + 1) * naccounts, naccounts);
 		executeStatement(con, sql.data);
 
 		executeStatement(con, "commit");
@@ -5258,6 +5374,32 @@ initGenerateDataServerSide(PGconn *con)
 	termPQExpBuffer(&sql);
 }
 
+/*
+ * Fill the standard tables with some data generated on the server
+ *
+ * As already the case with the client-side data generation, the filler
+ * column defaults to NULL in pgbench_branches and pgbench_tellers,
+ * and is a blank-padded string in pgbench_accounts.
+ */
+static void
+initGenerateDataServerSide(PGconn *con)
+{
+	fprintf(stderr, "generating data (server-side) ");
+
+	switch (data_generation_type)
+	{
+		case GEN_TYPE_INSERT_ORIGINAL:
+			generateDataInsertSingleTXN(con);
+			break;
+		case GEN_TYPE_INSERT_SERIES:
+			generateDataInsertSeries(con);
+			break;
+		case GEN_TYPE_INSERT_UNNEST:
+			generateDataInsertUnnest(con);
+			break;
+	}
+}
+
 /*
  * Invoke vacuum on the standard tables
  */
@@ -5341,6 +5483,8 @@ initCreateFKeys(PGconn *con)
 static void
 checkInitSteps(const char *initialize_steps)
 {
+	char	data_init_type = 0;
+
 	if (initialize_steps[0] == '\0')
 		pg_fatal("no initialization steps specified");
 
@@ -5352,7 +5496,26 @@ checkInitSteps(const char *initialize_steps)
 			pg_log_error_detail("Allowed step characters are: \"" ALL_INIT_STEPS "\".");
 			exit(1);
 		}
+
+		switch (*step)
+		{
+			case 'G':
+				data_init_type++;
+				data_generation_type = *step;
+				break;
+			case 'i':
+				data_init_type++;
+				data_generation_type = *step;
+				break;
+			case 'I':
+				data_init_type++;
+				data_generation_type = *step;
+				break;
+		}
 	}
+
+	if (data_init_type > 1)
+		pg_log_error("WARNING! More than one type of server-side data generation is requested");
 }
 
 /*
@@ -5395,6 +5558,8 @@ runInitSteps(const char *initialize_steps)
 				initGenerateDataClientSide(con);
 				break;
 			case 'G':
+			case 'i':
+			case 'I':
 				op = "server-side generate";
 				initGenerateDataServerSide(con);
 				break;
-- 
2.43.0


From 5e1827b889b283f50299ce6ab1a73f9f55a4a84f Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Mon, 10 Nov 2025 20:00:56 +0700
Subject: [PATCH 05/10] Fixing typo in query

---
 src/bin/pgbench/pgbench.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 65d77cdefea..03e37df4434 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -5241,7 +5241,8 @@ generateDataInsertSingleTXN(PGconn *con)
 	printfPQExpBuffer(&sql,
 					  "insert into pgbench_branches(bid, bbalance) "
 					  "select bid, 0 "
-					  "from generate_series(1, %d)", scale * nbranches);
+					  "from generate_series(1, %d) as bid",
+					  scale * nbranches);
 	executeStatement(con, sql.data);
 
 	printfPQExpBuffer(&sql,
-- 
2.43.0


From 7ca86521fda6929b8e0de3fc77dcbb8984009c88 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Tue, 11 Nov 2025 19:39:45 +0700
Subject: [PATCH 06/10] Adding support for COPY BINARY mode

---
 src/bin/pgbench/pgbench.c | 393 ++++++++++++++++++++++++++++++++++++--
 1 file changed, 381 insertions(+), 12 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 03e37df4434..71aa1d9479f 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -161,7 +161,7 @@ typedef struct socket_set
  * some configurable parameters */
 
 #define DEFAULT_INIT_STEPS "dtgvp"	/* default -I setting */
-#define ALL_INIT_STEPS "dtgGiIvpf"	/* all possible steps */
+#define ALL_INIT_STEPS "dtgCGiIvpf"	/* all possible steps */
 
 #define LOG_STEP_SECONDS	5	/* seconds between log messages */
 #define DEFAULT_NXACTS	10		/* default nxacts */
@@ -176,6 +176,8 @@ typedef struct socket_set
 /* 'one transaction per scale' server-side methods */
 #define GEN_TYPE_INSERT_SERIES		'i'	/* use INSERT .. SELECT generate_series to generate data */
 #define GEN_TYPE_INSERT_UNNEST  	'I'	/* use INSERT .. SELECT unnest to generate data */
+#define GEN_TYPE_COPY_ORIGINAL		'g' /* use COPY .. FROM STDIN .. TEXT to generate data */
+#define GEN_TYPE_COPY_BINARY		'C' /* use COPY .. FROM STDIN .. BINARY to generate data */
 
 static int	nxacts = 0;			/* number of transactions per client */
 static int	duration = 0;		/* duration in seconds */
@@ -188,10 +190,17 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
 static int	scale = 1;
 
 /*
- *
+ * mode of data generation to use
  */
 static char	data_generation_type = '?';
 
+/*
+ * COPY FROM BINARY execution buffer
+ */
+#define BIN_COPY_BUF_SIZE	102400				/* maximum buffer size for COPY FROM BINARY */
+static char		*bin_copy_buffer = NULL;		/* buffer for COPY FROM BINARY */
+static int32_t	 bin_copy_buffer_length = 0;	/* current buffer size */
+
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -861,7 +870,8 @@ static int	wait_on_socket_set(socket_set *sa, int64 usecs);
 static bool socket_has_input(socket_set *sa, int fd, int idx);
 
 /* callback used to build rows for COPY during data loading */
-typedef void (*initRowMethod) (PQExpBufferData *sql, int64 curr);
+typedef void (*initRowMethod)		(PQExpBufferData *sql, int64 curr);
+typedef void (*initRowMethodBin)	(PGconn *con, PGresult *res, int64_t curr, int32_t parent);
 
 /* callback functions for our flex lexer */
 static const PsqlScanCallbacks pgbench_callbacks = {
@@ -925,6 +935,7 @@ usage(void)
 		   "                           d: drop any existing pgbench tables\n"
 		   "                           t: create the tables used by the standard pgbench scenario\n"
 		   "                           g: generate data, client-side\n"
+		   "                           C:   client-side (single TNX) COPY .. FROM STDIN .. BINARY\n"
 		   "                           G: generate data, server-side in single transaction\n"
 		   "                           i:   server-side (multiple TXNs) INSERT .. SELECT generate_series\n"
 		   "                           I:   server-side (multiple TXNs) INSERT .. SELECT unnest\n"
@@ -5191,9 +5202,9 @@ initPopulateTable(PGconn *con, const char *table, int64 base,
  * a blank-padded string in pgbench_accounts.
  */
 static void
-initGenerateDataClientSide(PGconn *con)
+initGenerateDataClientSideText(PGconn *con)
 {
-	fprintf(stderr, "generating data (client-side)...\n");
+	fprintf(stderr, "TEXT mode...\n");
 
 	/*
 	 * we do all of this in one transaction to enable the backend's
@@ -5209,12 +5220,373 @@ initGenerateDataClientSide(PGconn *con)
 	 * already exist
 	 */
 	initPopulateTable(con, "pgbench_branches", nbranches, initBranch);
-	initPopulateTable(con, "pgbench_tellers", ntellers, initTeller);
+	initPopulateTable(con, "pgbench_tellers",  ntellers,  initTeller);
 	initPopulateTable(con, "pgbench_accounts", naccounts, initAccount);
 
 	executeStatement(con, "commit");
 }
 
+
+/*
+ * Dumps binary buffer to file (purely for debugging)
+ */
+static void
+dumpBufferToFile(char *filename)
+{
+	FILE *file_ptr;
+	size_t bytes_written;
+
+	file_ptr = fopen(filename, "wb");
+	if (file_ptr == NULL)
+	{
+		fprintf(stderr, "Error opening file %s\n", filename);
+		return; // EXIT_FAILURE;
+	}
+
+	bytes_written = fwrite(bin_copy_buffer, 1, bin_copy_buffer_length, file_ptr);
+
+	if (bytes_written != bin_copy_buffer_length)
+	{
+		fprintf(stderr, "Error writing to file or incomplete write\n");
+		fclose(file_ptr);
+		return; // EXIT_FAILURE;
+	}
+
+	fclose(file_ptr);
+}
+
+/*
+ * Save char data to buffer
+ */
+static void
+bufferCharData(char *src, int32_t len)
+{
+	memcpy((char *) bin_copy_buffer + bin_copy_buffer_length, (char *) src, len);
+	bin_copy_buffer_length += len;
+}
+
+/*
+ * Converts platform byte order into network byte order
+ * SPARC doesn't reqire that
+ */
+static void
+bufferData(void *src, int32_t len)
+{
+#ifdef __sparc__
+	bufferCharData(src, len);
+#else
+	if (len == 1)
+		bufferCharData(src, len);
+	else
+		for (int32_t i = 0; i < len; i++)
+		{
+			((char *) bin_copy_buffer + bin_copy_buffer_length)[i] =
+				((char *) src)[len - i - 1];
+		}
+
+	bin_copy_buffer_length += len;
+#endif
+}
+
+/*
+ * adds column counter
+ */
+static void
+addColumnCounter(int16_t n)
+{
+	bufferData((void *) &n, sizeof(n));
+}
+
+/*
+ * adds column with NULL value
+ */
+static void
+addNullColumn()
+{
+	int32_t null = -1;
+	bufferData((void *) &null, sizeof(null));
+}
+
+/*
+ * adds column with int8 value
+ */
+static void
+addInt8Column(int8_t value)
+{
+	int8_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with int16 value
+ */
+static void
+addInt16Column(int16_t value)
+{
+	int16_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with inti32 value
+ */
+static void
+addInt32Column(int32_t value)
+{
+	int32_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with inti64 value
+ */
+static void
+addInt64Column(int64_t value)
+{
+	int64_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with char value
+ */
+static void
+addCharColumn(char *value)
+{
+	int32_t	size = strlen(value);
+	bufferData((void *) &size, sizeof(size));
+	bufferCharData(value, size);
+}
+
+/*
+ * Starts communication with server for COPY FROM BINARY statement
+ */
+static void
+sendBinaryCopyHeader(PGconn *con)
+{
+	char header[] = {'P','G','C','O','P','Y','\n','\377','\r','\n','\0',
+					 '\0','\0','\0','\0',
+					 '\0','\0','\0','\0' };
+
+	PQputCopyData(con, header, 19);
+}
+
+/*
+ * Finishes communication with server for COPY FROM BINARY statement
+ */
+static void
+sendBinaryCopyTrailer(PGconn *con)
+{
+	static char trailer[] = { 0xFF, 0xFF };
+
+	PQputCopyData(con, trailer, 2);
+}
+
+/*
+ * Flashes current buffer over network if needed
+ */
+static void
+flushBuffer(PGconn *con, PGresult *res, int16_t row_len)
+{
+	if (bin_copy_buffer_length + row_len > BIN_COPY_BUF_SIZE)
+	{
+		/* flush current buffer */
+		if (PQresultStatus(res) == PGRES_COPY_IN)
+			PQputCopyData(con, (char *) bin_copy_buffer, bin_copy_buffer_length);
+		bin_copy_buffer_length = 0;
+	}
+}
+
+/*
+ * Sends current branch row to buffer
+ */
+static void
+initBranchBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len =  35 + 2 + 4*3; /* max row size is 32 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(2);
+
+	addInt32Column(curr + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Sends current teller row to buffer
+ */
+static void
+initTellerBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len =  40 + 2 + 4*4; /* max row size is 40 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(3);
+
+	addInt32Column(curr + 1);
+	addInt32Column(curr / parent + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Sends current account row to buffer
+ */
+static void
+initAccountBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len = 250 + 2 + 4*4; /* max row size is 250 for int64 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(3);
+
+	if (scale <= SCALE_32BIT_THRESHOLD)
+		addInt32Column(curr + 1);
+	else
+		addInt64Column(curr);
+
+	addInt32Column(curr / parent + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Universal wrapper for sending data in binary format
+ */
+static void
+initPopulateTableBinary(PGconn *con, char *table, char *columns,
+						int64_t base, initRowMethodBin init_row)
+{
+	int			 n;
+	PGresult	*res;
+	char		 copy_statement[256];
+	const char	*copy_statement_fmt = "copy %s (%s) from stdin (format binary)";
+	int64_t		 total = base * scale;
+
+	bin_copy_buffer_length = 0;
+
+	/* Use COPY with FREEZE on v14 and later for all ordinary tables */
+	if ((PQserverVersion(con) >= 140000) &&
+		get_table_relkind(con, table) == RELKIND_RELATION)
+		copy_statement_fmt = "copy %s (%s) from stdin with (format binary, freeze on)";
+
+	n = pg_snprintf(copy_statement, sizeof(copy_statement), copy_statement_fmt, table, columns);
+	if (n >= sizeof(copy_statement))
+		pg_fatal("invalid buffer size: must be at least %d characters long", n);
+	else if (n == -1)
+		pg_fatal("invalid format string");
+
+	res = PQexec(con, copy_statement);
+
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("unexpected copy in result: %s", PQerrorMessage(con));
+	PQclear(res);
+
+
+	sendBinaryCopyHeader(con);
+
+	for (int64_t i = 0; i < total; i++)
+	{
+		init_row(con, res, i, base);
+	}
+
+	if (PQresultStatus(res) == PGRES_COPY_IN)
+		PQputCopyData(con, (char *) bin_copy_buffer, bin_copy_buffer_length);
+	else
+		fprintf(stderr, "Unexpected mode %d instead of %d\n", PQresultStatus(res), PGRES_COPY_IN);
+
+	sendBinaryCopyTrailer(con);
+
+	if (PQresultStatus(res) == PGRES_COPY_IN)
+	{
+		if (PQputCopyEnd(con, NULL) == 1) /* success */
+		{
+			res = PQgetResult(con);
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				fprintf(stderr, "Error: %s\n", PQerrorMessage(con));
+			PQclear(res);
+		}
+		else
+			fprintf(stderr, "Error: %s\n", PQerrorMessage(con));
+	}
+}
+
+/*
+ * Wrapper for binary data load
+ */
+static void
+initGenerateDataClientSideBinary(PGconn *con)
+{
+
+	fprintf(stderr, "BINARY mode...\n");
+
+	bin_copy_buffer = pg_malloc(BIN_COPY_BUF_SIZE);
+	bin_copy_buffer_length = 0;
+
+	/*
+	 * we do all of this in one transaction to enable the backend's
+	 * data-loading optimizations
+	 */
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	initPopulateTableBinary(con, "pgbench_branches", "bid, bbalance",
+							nbranches, initBranchBinary);
+	initPopulateTableBinary(con, "pgbench_tellers",  "tid, bid, tbalance",
+							ntellers,  initTellerBinary);
+	initPopulateTableBinary(con, "pgbench_accounts", "aid, bid, abalance",
+							naccounts, initAccountBinary);
+
+	executeStatement(con, "commit");
+
+	pg_free(bin_copy_buffer);
+}
+
+/*
+ * Fill the standard tables with some data generated and sent from the client.
+ */
+static void
+initGenerateDataClientSide(PGconn *con)
+{
+	fprintf(stderr, "generating data (client-side) in ");
+
+	switch (data_generation_type)
+	{
+		case GEN_TYPE_COPY_ORIGINAL:
+			initGenerateDataClientSideText(con);
+			break;
+		case GEN_TYPE_COPY_BINARY:
+			initGenerateDataClientSideBinary(con);
+			break;
+	}
+}
+
 /*
  * Generating data via INSERT .. SELECT .. FROM generate_series
  * whole dataset in single transaction
@@ -5500,14 +5872,10 @@ checkInitSteps(const char *initialize_steps)
 
 		switch (*step)
 		{
+			case 'g':
+			case 'C':
 			case 'G':
-				data_init_type++;
-				data_generation_type = *step;
-				break;
 			case 'i':
-				data_init_type++;
-				data_generation_type = *step;
-				break;
 			case 'I':
 				data_init_type++;
 				data_generation_type = *step;
@@ -5555,6 +5923,7 @@ runInitSteps(const char *initialize_steps)
 				initCreateTables(con);
 				break;
 			case 'g':
+			case 'C':
 				op = "client-side generate";
 				initGenerateDataClientSide(con);
 				break;
-- 
2.43.0


From 4aa0ac05765edf6b5f0c13e18ac677287ce78206 Mon Sep 17 00:00:00 2001
From: Fujii Masao <fujii@postgresql.org>
Date: Fri, 14 Nov 2025 22:40:39 +0900
Subject: [PATCH 07/10] pgbench: Fix assertion failure with multiple
 \syncpipeline in pipeline mode.

Previously, when pgbench ran a custom script that triggered retriable errors
(e.g., deadlocks) followed by multiple \syncpipeline commands in pipeline mode,
the following assertion failure could occur:

    Assertion failed: (res == ((void*)0)), function discardUntilSync, file pgbench.c, line 3594.

The issue was that discardUntilSync() assumed a pipeline sync result
(PGRES_PIPELINE_SYNC) would always be followed by either another sync result
or NULL. This assumption was incorrect: when multiple sync requests were sent,
a sync result could instead be followed by another result type. In such cases,
discardUntilSync() mishandled the results, leading to the assertion failure.

This commit fixes the issue by making discardUntilSync() correctly handle cases
where a pipeline sync result is followed by other result types. It now continues
discarding results until another pipeline sync followed by NULL is reached.

Backpatched to v17, where support for \syncpipeline command in pgbench was
introduced.

Author: Yugo Nagata <nagata@sraoss.co.jp>
Reviewed-by: Chao Li <lic@highgo.com>
Reviewed-by: Fujii Masao <masao.fujii@gmail.com>
Discussion: https://postgr.es/m/20251111105037.f3fc554616bc19891f926c5b@sraoss.co.jp
Backpatch-through: 17
---
 src/bin/pgbench/pgbench.c | 39 ++++++++++++++++++++++++++++-----------
 1 file changed, 28 insertions(+), 11 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index d8764ba6fe0..a425176ecdc 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -3563,14 +3563,18 @@ doRetry(CState *st, pg_time_usec_t *now)
 }
 
 /*
- * Read results and discard it until a sync point.
+ * Read and discard results until the last sync point.
  */
 static int
 discardUntilSync(CState *st)
 {
 	bool		received_sync = false;
 
-	/* send a sync */
+	/*
+	 * Send a Sync message to ensure at least one PGRES_PIPELINE_SYNC is
+	 * received and to avoid an infinite loop, since all earlier ones may have
+	 * already been received.
+	 */
 	if (!PQpipelineSync(st->con))
 	{
 		pg_log_error("client %d aborted: failed to send a pipeline sync",
@@ -3578,29 +3582,42 @@ discardUntilSync(CState *st)
 		return 0;
 	}
 
-	/* receive PGRES_PIPELINE_SYNC and null following it */
+	/*
+	 * Continue reading results until the last sync point, i.e., until
+	 * reaching null just after PGRES_PIPELINE_SYNC.
+	 */
 	for (;;)
 	{
 		PGresult   *res = PQgetResult(st->con);
 
+		if (PQstatus(st->con) == CONNECTION_BAD)
+		{
+			pg_log_error("client %d aborted while rolling back the transaction after an error; perhaps the backend died while processing",
+						 st->id);
+			PQclear(res);
+			return 0;
+		}
+
 		if (PQresultStatus(res) == PGRES_PIPELINE_SYNC)
 			received_sync = true;
-		else if (received_sync)
+		else if (received_sync && res == NULL)
 		{
-			/*
-			 * PGRES_PIPELINE_SYNC must be followed by another
-			 * PGRES_PIPELINE_SYNC or NULL; otherwise, assert failure.
-			 */
-			Assert(res == NULL);
-
 			/*
 			 * Reset ongoing sync count to 0 since all PGRES_PIPELINE_SYNC
 			 * results have been discarded.
 			 */
 			st->num_syncs = 0;
-			PQclear(res);
 			break;
 		}
+		else
+		{
+			/*
+			 * If a PGRES_PIPELINE_SYNC is followed by something other than
+			 * PGRES_PIPELINE_SYNC or NULL, another PGRES_PIPELINE_SYNC will
+			 * appear later. Reset received_sync to false to wait for it.
+			 */
+			received_sync = false;
+		}
 		PQclear(res);
 	}
 
-- 
2.43.0


From 9c4f19055597e9adb25e65c2aa8bedf20a09e13d Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Fri, 21 Nov 2025 19:05:58 +0700
Subject: [PATCH 08/10] Setting empty string as default value in filler column

---
 src/bin/pgbench/pgbench.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 967f6ce6984..03b5e5c28f0 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -4985,26 +4985,26 @@ initCreateTables(PGconn *con)
 	static const struct ddlinfo DDLs[] = {
 		{
 			"pgbench_history",
-			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22)",
-			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22)",
+			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22) default ''",
+			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22) default ''",
 			0
 		},
 		{
 			"pgbench_tellers",
-			"tid int not null,bid int,tbalance int,filler char(84)",
-			"tid int not null,bid int,tbalance int,filler char(84)",
+			"tid int not null,bid int,tbalance int,filler char(84) default ''",
+			"tid int not null,bid int,tbalance int,filler char(84) default ''",
 			1
 		},
 		{
 			"pgbench_accounts",
-			"aid    int not null,bid int,abalance int,filler char(84)",
-			"aid bigint not null,bid int,abalance int,filler char(84)",
+			"aid    int not null,bid int,abalance int,filler char(84) default ''",
+			"aid bigint not null,bid int,abalance int,filler char(84) default ''",
 			1
 		},
 		{
 			"pgbench_branches",
-			"bid int not null,bbalance int,filler char(88)",
-			"bid int not null,bbalance int,filler char(88)",
+			"bid int not null,bbalance int,filler char(88) default ''",
+			"bid int not null,bbalance int,filler char(88) default ''",
 			1
 		}
 	};
-- 
2.43.0


From dcb85d26f8132eaaf9d096e814b9bda49db7d478 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Fri, 21 Nov 2025 20:06:24 +0700
Subject: [PATCH 09/10] Switching COPY FROM BINARY ti run in multiple
 transactions

---
 src/bin/pgbench/pgbench.c | 27 ++++++++++++++++-----------
 1 file changed, 16 insertions(+), 11 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 03b5e5c28f0..6b89007a63b 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -5496,20 +5496,20 @@ initAccountBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
  */
 static void
 initPopulateTableBinary(PGconn *con, char *table, char *columns,
-						int64_t base, initRowMethodBin init_row)
+						int counter, int64_t base, initRowMethodBin init_row)
 {
 	int			 n;
 	PGresult	*res;
 	char		 copy_statement[256];
 	const char	*copy_statement_fmt = "copy %s (%s) from stdin (format binary)";
-	int64_t		 total = base * scale;
+	int64_t		 start = base * counter;
 
 	bin_copy_buffer_length = 0;
 
 	/* Use COPY with FREEZE on v14 and later for all ordinary tables */
 	if ((PQserverVersion(con) >= 140000) &&
 		get_table_relkind(con, table) == RELKIND_RELATION)
-		copy_statement_fmt = "copy %s (%s) from stdin with (format binary, freeze on)";
+		copy_statement_fmt = "copy %s (%s) from stdin with (format binary)";
 
 	n = pg_snprintf(copy_statement, sizeof(copy_statement), copy_statement_fmt, table, columns);
 	if (n >= sizeof(copy_statement))
@@ -5526,7 +5526,7 @@ initPopulateTableBinary(PGconn *con, char *table, char *columns,
 
 	sendBinaryCopyHeader(con);
 
-	for (int64_t i = 0; i < total; i++)
+	for (int64_t i = start; i < start + base; i++)
 	{
 		init_row(con, res, i, base);
 	}
@@ -5573,15 +5573,20 @@ initGenerateDataClientSideBinary(PGconn *con)
 	/* truncate away any old data */
 	initTruncateTables(con);
 
-	initPopulateTableBinary(con, "pgbench_branches", "bid, bbalance",
-							nbranches, initBranchBinary);
-	initPopulateTableBinary(con, "pgbench_tellers",  "tid, bid, tbalance",
-							ntellers,  initTellerBinary);
-	initPopulateTableBinary(con, "pgbench_accounts", "aid, bid, abalance",
-							naccounts, initAccountBinary);
-
 	executeStatement(con, "commit");
 
+	for (int i = 0; i < scale; i++)
+	{
+		initPopulateTableBinary(con, "pgbench_branches", "bid, bbalance",
+								i, nbranches, initBranchBinary);
+		initPopulateTableBinary(con, "pgbench_tellers",  "tid, bid, tbalance",
+								i, ntellers,  initTellerBinary);
+		initPopulateTableBinary(con, "pgbench_accounts", "aid, bid, abalance",
+								i, naccounts, initAccountBinary);
+
+		executeStatement(con, "commit");
+	}
+
 	pg_free(bin_copy_buffer);
 }
 
-- 
2.43.0


From b8e28881225234fd00b55235bc60fad2dc60b544 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Sat, 22 Nov 2025 17:06:00 +0700
Subject: [PATCH 10/10] Adding tests for new modes of data generation

---
 src/bin/pgbench/pgbench.c                    | 21 ++++----
 src/bin/pgbench/t/001_pgbench_with_server.pl | 52 +++++++++++++++++---
 2 files changed, 56 insertions(+), 17 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 6b89007a63b..dd4e5d5e056 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -164,7 +164,7 @@ typedef struct socket_set
 #define ALL_INIT_STEPS "dtgCGiIvpf"	/* all possible steps */
 
 #define LOG_STEP_SECONDS	5	/* seconds between log messages */
-#define DEFAULT_NXACTS	10		/* default nxacts */
+#define DEFAULT_NXACTS		10	/* default nxacts */
 
 #define MIN_GAUSSIAN_PARAM		2.0 /* minimum parameter for gauss */
 
@@ -192,7 +192,7 @@ static int	scale = 1;
 /*
  * mode of data generation to use
  */
-static char	data_generation_type = '?';
+static char	data_generation_type = GEN_TYPE_COPY_ORIGINAL;
 
 /*
  * COPY FROM BINARY execution buffer
@@ -4985,26 +4985,26 @@ initCreateTables(PGconn *con)
 	static const struct ddlinfo DDLs[] = {
 		{
 			"pgbench_history",
-			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22) default ''",
-			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22) default ''",
+			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22) default '?'",
+			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22) default '?'",
 			0
 		},
 		{
 			"pgbench_tellers",
-			"tid int not null,bid int,tbalance int,filler char(84) default ''",
-			"tid int not null,bid int,tbalance int,filler char(84) default ''",
+			"tid int not null,bid int,tbalance int,filler char(84)",
+			"tid int not null,bid int,tbalance int,filler char(84)",
 			1
 		},
 		{
 			"pgbench_accounts",
-			"aid    int not null,bid int,abalance int,filler char(84) default ''",
-			"aid bigint not null,bid int,abalance int,filler char(84) default ''",
+			"aid    int not null,bid int,abalance int,filler char(84) default '?'",
+			"aid bigint not null,bid int,abalance int,filler char(84) default '?'",
 			1
 		},
 		{
 			"pgbench_branches",
-			"bid int not null,bbalance int,filler char(88) default ''",
-			"bid int not null,bbalance int,filler char(88) default ''",
+			"bid int not null,bbalance int,filler char(88)",
+			"bid int not null,bbalance int,filler char(88)",
 			1
 		}
 	};
@@ -7837,6 +7837,7 @@ main(int argc, char **argv)
 			}
 		}
 
+		checkInitSteps(initialize_steps);
 		runInitSteps(initialize_steps);
 		exit(0);
 	}
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
index 581e9af7907..a377048ead1 100644
--- a/src/bin/pgbench/t/001_pgbench_with_server.pl
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -16,25 +16,30 @@ sub check_data_state
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 	my $node = shift;
 	my $type = shift;
+	my $sql_result;
 
-	my $sql_result = $node->safe_psql('postgres',
-		'SELECT count(*) AS null_count FROM pgbench_accounts WHERE filler IS NULL LIMIT 10;'
-	);
-	is($sql_result, '0',
-		"$type: filler column of pgbench_accounts has no NULL data");
 	$sql_result = $node->safe_psql('postgres',
 		'SELECT count(*) AS null_count FROM pgbench_branches WHERE filler IS NULL;'
 	);
 	is($sql_result, '1',
 		"$type: filler column of pgbench_branches has only NULL data");
+
 	$sql_result = $node->safe_psql('postgres',
 		'SELECT count(*) AS null_count FROM pgbench_tellers WHERE filler IS NULL;'
 	);
 	is($sql_result, '10',
 		"$type: filler column of pgbench_tellers has only NULL data");
+
+	$sql_result = $node->safe_psql('postgres',
+		'SELECT count(*) AS null_count FROM pgbench_accounts WHERE filler IS NULL LIMIT 10;'
+	);
+	is($sql_result, '0',
+		"$type: filler column of pgbench_accounts has no NULL data");
+
 	$sql_result = $node->safe_psql('postgres',
 		'SELECT count(*) AS data_count FROM pgbench_history;');
-	is($sql_result, '0', "$type: pgbench_history has no data");
+	is($sql_result, '0',
+		"$type: pgbench_history has no data");
 }
 
 # start a pgbench specific server
@@ -125,7 +130,7 @@ $node->pgbench(
 	'pgbench scale 1 initialization',);
 
 # Check data state, after client-side data generation.
-check_data_state($node, 'client-side');
+check_data_state($node, 'client-side (default options)');
 
 # Again, with all possible options
 $node->pgbench(
@@ -143,6 +148,7 @@ $node->pgbench(
 		qr{done in \d+\.\d\d s }
 	],
 	'pgbench scale 1 initialization');
+check_data_state($node, 'client-side (all options)');
 
 # Test interaction of --init-steps with legacy step-selection options
 $node->pgbench(
@@ -164,6 +170,38 @@ $node->pgbench(
 # Check data state, after server-side data generation.
 check_data_state($node, 'server-side');
 
+# Test server-side generation with UNNEST
+$node->pgbench(
+	'--initialize --init-steps=dtI',
+	0,
+	[qr{^$}],
+	[
+		qr{dropping old tables},
+		qr{creating tables},
+		qr{generating data \(server-side\)},
+		qr{done in \d+\.\d\d s }
+	],
+	'pgbench --init-steps server-side UNNEST');
+
+# Check data state, after server-side data generation.
+check_data_state($node, 'server-side (unnest)');
+
+# Test server-side generation with UNNEST
+$node->pgbench(
+	'--initialize --init-steps=dtC',
+	0,
+	[qr{^$}],
+	[
+		qr{dropping old tables},
+		qr{creating tables},
+		qr{generating data \(client-side\)},
+		qr{done in \d+\.\d\d s }
+	],
+	'pgbench --init-steps client-side BINARY');
+
+# Check data state, after server-side data generation.
+check_data_state($node, 'client-side (binary)');
+
 # Run all builtin scripts, for a few transactions each
 $node->pgbench(
 	'--transactions=5 -Dfoo=bla --client=2 --protocol=simple --builtin=t'
-- 
2.43.0

#8Boris Mironov
boris_mironov@outlook.com
In reply to: Boris Mironov (#7)
1 attachment(s)
Re: Idea to enhance pgbench by more modes to generate data (multi-TXNs, UNNEST, COPY BINARY)

Hello,

Updating code to satisfy compiler warnings about unused code.

Cheers,
Boris

Attachments:

v4-pgbench-faster-modes.patchapplication/octet-stream; name=v4-pgbench-faster-modes.patchDownload
From c768f399c556295de7d53895410e686d86b4b960 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Sun, 9 Nov 2025 19:34:58 +0700
Subject: [PATCH 01/12] Converting one huge transaction into series of one per
 'scale'

---
 src/bin/pgbench/pgbench.c | 61 ++++++++++++++++++++++++++-------------
 1 file changed, 41 insertions(+), 20 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index d8764ba6fe0..284a7c860f1 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -181,6 +181,12 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
  */
 static int	scale = 1;
 
+/*
+ * scaling factor after which we switch to multiple transactions during
+ * data population phase on server side
+ */
+static int64	single_txn_scale_limit = 1;
+
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -5213,6 +5219,7 @@ static void
 initGenerateDataServerSide(PGconn *con)
 {
 	PQExpBufferData sql;
+	int				chunk = (scale >= single_txn_scale_limit) ? 1 : scale;
 
 	fprintf(stderr, "generating data (server-side)...\n");
 
@@ -5225,30 +5232,44 @@ initGenerateDataServerSide(PGconn *con)
 	/* truncate away any old data */
 	initTruncateTables(con);
 
+	executeStatement(con, "commit");
+
 	initPQExpBuffer(&sql);
 
-	printfPQExpBuffer(&sql,
-					  "insert into pgbench_branches(bid,bbalance) "
-					  "select bid, 0 "
-					  "from generate_series(1, %d) as bid", nbranches * scale);
-	executeStatement(con, sql.data);
-
-	printfPQExpBuffer(&sql,
-					  "insert into pgbench_tellers(tid,bid,tbalance) "
-					  "select tid, (tid - 1) / %d + 1, 0 "
-					  "from generate_series(1, %d) as tid", ntellers, ntellers * scale);
-	executeStatement(con, sql.data);
-
-	printfPQExpBuffer(&sql,
-					  "insert into pgbench_accounts(aid,bid,abalance,filler) "
-					  "select aid, (aid - 1) / %d + 1, 0, '' "
-					  "from generate_series(1, " INT64_FORMAT ") as aid",
-					  naccounts, (int64) naccounts * scale);
-	executeStatement(con, sql.data);
+	for (int i = 0; i < scale; i += chunk) {
+		executeStatement(con, "begin");
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_branches(bid,bbalance) "
+						  "select bid + 1, 0 "
+						  "from generate_series(%d, %d) as bid", i, i + chunk);
+						  //"select bid, 0 "
+						  //"from generate_series(1, %d) as bid", nbranches * scale);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_tellers(tid,bid,tbalance) "
+						  "select tid + 1, tid / %d + 1, 0 "
+						  "from generate_series(%d, %d) as tid",
+						  ntellers, i * ntellers, (i + chunk) * ntellers - 1);
+						  //"select tid, (tid - 1) / %d + 1, 0 "
+						  //"from generate_series(1, %d) as tid", ntellers, ntellers * scale);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_accounts(aid,bid,abalance,filler) "
+						  "select aid + 1, aid / %d + 1, 0, '' "
+						  "from generate_series(" INT64_FORMAT ", " INT64_FORMAT ") as aid",
+						  naccounts, (int64) i * naccounts, (int64) (i + chunk) * naccounts - 1);
+						  //"select aid, (aid - 1) / %d + 1, 0, '' "
+						  //"from generate_series(1, " INT64_FORMAT ") as aid",
+						  //naccounts, (int64) naccounts * scale);
+		executeStatement(con, sql.data);
+
+		executeStatement(con, "commit");
+	}
 
 	termPQExpBuffer(&sql);
-
-	executeStatement(con, "commit");
 }
 
 /*
-- 
2.43.0


From 0eddb156c187d829c4381bc928c5314705928852 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Sun, 9 Nov 2025 20:13:23 +0700
Subject: [PATCH 02/12] Getting rid off limit for single transaction size
 during data generation

---
 src/bin/pgbench/pgbench.c | 15 ++++-----------
 1 file changed, 4 insertions(+), 11 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 284a7c860f1..28b72e4cf1f 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -181,12 +181,6 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
  */
 static int	scale = 1;
 
-/*
- * scaling factor after which we switch to multiple transactions during
- * data population phase on server side
- */
-static int64	single_txn_scale_limit = 1;
-
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -5219,7 +5213,6 @@ static void
 initGenerateDataServerSide(PGconn *con)
 {
 	PQExpBufferData sql;
-	int				chunk = (scale >= single_txn_scale_limit) ? 1 : scale;
 
 	fprintf(stderr, "generating data (server-side)...\n");
 
@@ -5236,13 +5229,13 @@ initGenerateDataServerSide(PGconn *con)
 
 	initPQExpBuffer(&sql);
 
-	for (int i = 0; i < scale; i += chunk) {
+	for (int i = 0; i < scale; i++) {
 		executeStatement(con, "begin");
 
 		printfPQExpBuffer(&sql,
 						  "insert into pgbench_branches(bid,bbalance) "
 						  "select bid + 1, 0 "
-						  "from generate_series(%d, %d) as bid", i, i + chunk);
+						  "from generate_series(%d, %d) as bid", i, i + 1);
 						  //"select bid, 0 "
 						  //"from generate_series(1, %d) as bid", nbranches * scale);
 		executeStatement(con, sql.data);
@@ -5251,7 +5244,7 @@ initGenerateDataServerSide(PGconn *con)
 						  "insert into pgbench_tellers(tid,bid,tbalance) "
 						  "select tid + 1, tid / %d + 1, 0 "
 						  "from generate_series(%d, %d) as tid",
-						  ntellers, i * ntellers, (i + chunk) * ntellers - 1);
+						  ntellers, i * ntellers, (i + 1) * ntellers - 1);
 						  //"select tid, (tid - 1) / %d + 1, 0 "
 						  //"from generate_series(1, %d) as tid", ntellers, ntellers * scale);
 		executeStatement(con, sql.data);
@@ -5260,7 +5253,7 @@ initGenerateDataServerSide(PGconn *con)
 						  "insert into pgbench_accounts(aid,bid,abalance,filler) "
 						  "select aid + 1, aid / %d + 1, 0, '' "
 						  "from generate_series(" INT64_FORMAT ", " INT64_FORMAT ") as aid",
-						  naccounts, (int64) i * naccounts, (int64) (i + chunk) * naccounts - 1);
+						  naccounts, (int64) i * naccounts, (int64) (i + 1) * naccounts - 1);
 						  //"select aid, (aid - 1) / %d + 1, 0, '' "
 						  //"from generate_series(1, " INT64_FORMAT ") as aid",
 						  //naccounts, (int64) naccounts * scale);
-- 
2.43.0


From c5659cf474ec273c057668f30a4f435fd02f2da7 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Sun, 9 Nov 2025 20:38:36 +0700
Subject: [PATCH 03/12] No need to keep old code in comments

---
 src/bin/pgbench/pgbench.c | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 28b72e4cf1f..97895aa9edf 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -5236,8 +5236,6 @@ initGenerateDataServerSide(PGconn *con)
 						  "insert into pgbench_branches(bid,bbalance) "
 						  "select bid + 1, 0 "
 						  "from generate_series(%d, %d) as bid", i, i + 1);
-						  //"select bid, 0 "
-						  //"from generate_series(1, %d) as bid", nbranches * scale);
 		executeStatement(con, sql.data);
 
 		printfPQExpBuffer(&sql,
@@ -5245,8 +5243,6 @@ initGenerateDataServerSide(PGconn *con)
 						  "select tid + 1, tid / %d + 1, 0 "
 						  "from generate_series(%d, %d) as tid",
 						  ntellers, i * ntellers, (i + 1) * ntellers - 1);
-						  //"select tid, (tid - 1) / %d + 1, 0 "
-						  //"from generate_series(1, %d) as tid", ntellers, ntellers * scale);
 		executeStatement(con, sql.data);
 
 		printfPQExpBuffer(&sql,
@@ -5254,9 +5250,6 @@ initGenerateDataServerSide(PGconn *con)
 						  "select aid + 1, aid / %d + 1, 0, '' "
 						  "from generate_series(" INT64_FORMAT ", " INT64_FORMAT ") as aid",
 						  naccounts, (int64) i * naccounts, (int64) (i + 1) * naccounts - 1);
-						  //"select aid, (aid - 1) / %d + 1, 0, '' "
-						  //"from generate_series(1, " INT64_FORMAT ") as aid",
-						  //naccounts, (int64) naccounts * scale);
 		executeStatement(con, sql.data);
 
 		executeStatement(con, "commit");
-- 
2.43.0


From e47b52ddf23593dad9375ef5356fd41d0621ede3 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Mon, 10 Nov 2025 19:06:48 +0700
Subject: [PATCH 04/12] Adding server-side data generation via unnest

---
 src/bin/pgbench/pgbench.c | 199 ++++++++++++++++++++++++++++++++++----
 1 file changed, 182 insertions(+), 17 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 97895aa9edf..65d77cdefea 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -161,7 +161,7 @@ typedef struct socket_set
  * some configurable parameters */
 
 #define DEFAULT_INIT_STEPS "dtgvp"	/* default -I setting */
-#define ALL_INIT_STEPS "dtgGvpf"	/* all possible steps */
+#define ALL_INIT_STEPS "dtgGiIvpf"	/* all possible steps */
 
 #define LOG_STEP_SECONDS	5	/* seconds between log messages */
 #define DEFAULT_NXACTS	10		/* default nxacts */
@@ -171,6 +171,12 @@ typedef struct socket_set
 #define MIN_ZIPFIAN_PARAM		1.001	/* minimum parameter for zipfian */
 #define MAX_ZIPFIAN_PARAM		1000.0	/* maximum parameter for zipfian */
 
+/* original single transaction server-side method */
+#define GEN_TYPE_INSERT_ORIGINAL	'G'	/* use INSERT .. SELECT generate_series to generate data */
+/* 'one transaction per scale' server-side methods */
+#define GEN_TYPE_INSERT_SERIES		'i'	/* use INSERT .. SELECT generate_series to generate data */
+#define GEN_TYPE_INSERT_UNNEST  	'I'	/* use INSERT .. SELECT unnest to generate data */
+
 static int	nxacts = 0;			/* number of transactions per client */
 static int	duration = 0;		/* duration in seconds */
 static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
@@ -181,6 +187,11 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
  */
 static int	scale = 1;
 
+/*
+ *
+ */
+static char	data_generation_type = '?';
+
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -914,7 +925,9 @@ usage(void)
 		   "                           d: drop any existing pgbench tables\n"
 		   "                           t: create the tables used by the standard pgbench scenario\n"
 		   "                           g: generate data, client-side\n"
-		   "                           G: generate data, server-side\n"
+		   "                           G: generate data, server-side in single transaction\n"
+		   "                           i:   server-side (multiple TXNs) INSERT .. SELECT generate_series\n"
+		   "                           I:   server-side (multiple TXNs) INSERT .. SELECT unnest\n"
 		   "                           v: invoke VACUUM on the standard tables\n"
 		   "                           p: create primary key indexes on the standard tables\n"
 		   "                           f: create foreign keys between the standard tables\n"
@@ -5203,18 +5216,16 @@ initGenerateDataClientSide(PGconn *con)
 }
 
 /*
- * Fill the standard tables with some data generated on the server
- *
- * As already the case with the client-side data generation, the filler
- * column defaults to NULL in pgbench_branches and pgbench_tellers,
- * and is a blank-padded string in pgbench_accounts.
+ * Generating data via INSERT .. SELECT .. FROM generate_series
+ * whole dataset in single transaction
  */
 static void
-initGenerateDataServerSide(PGconn *con)
+generateDataInsertSingleTXN(PGconn *con)
 {
 	PQExpBufferData sql;
 
-	fprintf(stderr, "generating data (server-side)...\n");
+	fprintf(stderr, "via INSERT .. SELECT generate_series... in single TXN\n");
+
 
 	/*
 	 * we do all of this in one transaction to enable the backend's
@@ -5225,31 +5236,136 @@ initGenerateDataServerSide(PGconn *con)
 	/* truncate away any old data */
 	initTruncateTables(con);
 
+	initPQExpBuffer(&sql);
+
+	printfPQExpBuffer(&sql,
+					  "insert into pgbench_branches(bid, bbalance) "
+					  "select bid, 0 "
+					  "from generate_series(1, %d)", scale * nbranches);
+	executeStatement(con, sql.data);
+
+	printfPQExpBuffer(&sql,
+					  "insert into pgbench_tellers(tid, bid, tbalance) "
+					  "select tid + 1, tid / %d + 1, 0 "
+					  "from generate_series(0, %d) as tid",
+					  ntellers, (scale * ntellers) - 1);
+	executeStatement(con, sql.data);
+
+	printfPQExpBuffer(&sql,
+					  "insert into pgbench_accounts(aid, bid, abalance, "
+								   "filler) "
+					  "select aid + 1, aid / %d + 1, 0, '' "
+					  "from generate_series(0, " INT64_FORMAT ") as aid",
+					  naccounts, (int64) (scale * naccounts) - 1);
+	executeStatement(con, sql.data);
+
 	executeStatement(con, "commit");
 
+	termPQExpBuffer(&sql);
+}
+
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM generate_series
+ * One transaction per 'scale'
+ */
+static void
+generateDataInsertSeries(PGconn *con)
+{
+	PQExpBufferData sql;
+
+	fprintf(stderr, "via INSERT .. SELECT generate_series... in multiple TXN(s)\n");
+
 	initPQExpBuffer(&sql);
 
-	for (int i = 0; i < scale; i++) {
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	executeStatement(con, "commit");
+
+	for (int i = 0; i < scale; i++)
+	{
 		executeStatement(con, "begin");
 
 		printfPQExpBuffer(&sql,
-						  "insert into pgbench_branches(bid,bbalance) "
-						  "select bid + 1, 0 "
-						  "from generate_series(%d, %d) as bid", i, i + 1);
+						  "insert into pgbench_branches(bid, bbalance) "
+						  "values(%d, 0)", i + 1);
 		executeStatement(con, sql.data);
 
 		printfPQExpBuffer(&sql,
-						  "insert into pgbench_tellers(tid,bid,tbalance) "
+						  "insert into pgbench_tellers(tid, bid, tbalance) "
 						  "select tid + 1, tid / %d + 1, 0 "
 						  "from generate_series(%d, %d) as tid",
 						  ntellers, i * ntellers, (i + 1) * ntellers - 1);
 		executeStatement(con, sql.data);
 
 		printfPQExpBuffer(&sql,
-						  "insert into pgbench_accounts(aid,bid,abalance,filler) "
+						  "insert into pgbench_accounts(aid, bid, abalance, "
+									   "filler) "
 						  "select aid + 1, aid / %d + 1, 0, '' "
-						  "from generate_series(" INT64_FORMAT ", " INT64_FORMAT ") as aid",
-						  naccounts, (int64) i * naccounts, (int64) (i + 1) * naccounts - 1);
+						  "from generate_series(" INT64_FORMAT ", "
+								INT64_FORMAT ") as aid",
+						  naccounts, (int64) i * naccounts,
+						  (int64) (i + 1) * naccounts - 1);
+		executeStatement(con, sql.data);
+
+		executeStatement(con, "commit");
+	}
+
+	termPQExpBuffer(&sql);
+}
+
+/*
+ * Generating data via INSERT .. SELECT .. FROM unnest
+ * One transaction per 'scale'
+ */
+static void
+generateDataInsertUnnest(PGconn *con)
+{
+	PQExpBufferData sql;
+
+	fprintf(stderr, "via INSERT .. SELECT unnest...\n");
+
+	initPQExpBuffer(&sql);
+
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	executeStatement(con, "commit");
+
+	for (int s = 0; s < scale; s++)
+	{
+		executeStatement(con, "begin");
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_branches(bid,bbalance) "
+						  "values(%d, 0)", s + 1);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "insert into pgbench_tellers(tid, bid, tbalance) "
+						  "select unnest(array_agg(s.i order by s.i)) as tid, "
+								  "%d as bid, 0 as tbalance "
+						  "from generate_series(%d, %d) as s(i)",
+						  s + 1, s * ntellers + 1, (s + 1) * ntellers);
+		executeStatement(con, sql.data);
+
+		printfPQExpBuffer(&sql,
+						  "with data as ("
+						  "   select generate_series(" INT64_FORMAT ", "
+							  INT64_FORMAT ") as i) "
+						  "insert into pgbench_accounts(aid, bid, "
+									  "abalance, filler) "
+						  "select unnest(aid), unnest(bid), 0 as abalance, "
+								  "'' as filler "
+						  "from (select array_agg(i+1) aid, "
+									   "array_agg(i/%d + 1) bid from data)",
+						  (int64) s * naccounts + 1,
+						  (int64) (s + 1) * naccounts, naccounts);
 		executeStatement(con, sql.data);
 
 		executeStatement(con, "commit");
@@ -5258,6 +5374,32 @@ initGenerateDataServerSide(PGconn *con)
 	termPQExpBuffer(&sql);
 }
 
+/*
+ * Fill the standard tables with some data generated on the server
+ *
+ * As already the case with the client-side data generation, the filler
+ * column defaults to NULL in pgbench_branches and pgbench_tellers,
+ * and is a blank-padded string in pgbench_accounts.
+ */
+static void
+initGenerateDataServerSide(PGconn *con)
+{
+	fprintf(stderr, "generating data (server-side) ");
+
+	switch (data_generation_type)
+	{
+		case GEN_TYPE_INSERT_ORIGINAL:
+			generateDataInsertSingleTXN(con);
+			break;
+		case GEN_TYPE_INSERT_SERIES:
+			generateDataInsertSeries(con);
+			break;
+		case GEN_TYPE_INSERT_UNNEST:
+			generateDataInsertUnnest(con);
+			break;
+	}
+}
+
 /*
  * Invoke vacuum on the standard tables
  */
@@ -5341,6 +5483,8 @@ initCreateFKeys(PGconn *con)
 static void
 checkInitSteps(const char *initialize_steps)
 {
+	char	data_init_type = 0;
+
 	if (initialize_steps[0] == '\0')
 		pg_fatal("no initialization steps specified");
 
@@ -5352,7 +5496,26 @@ checkInitSteps(const char *initialize_steps)
 			pg_log_error_detail("Allowed step characters are: \"" ALL_INIT_STEPS "\".");
 			exit(1);
 		}
+
+		switch (*step)
+		{
+			case 'G':
+				data_init_type++;
+				data_generation_type = *step;
+				break;
+			case 'i':
+				data_init_type++;
+				data_generation_type = *step;
+				break;
+			case 'I':
+				data_init_type++;
+				data_generation_type = *step;
+				break;
+		}
 	}
+
+	if (data_init_type > 1)
+		pg_log_error("WARNING! More than one type of server-side data generation is requested");
 }
 
 /*
@@ -5395,6 +5558,8 @@ runInitSteps(const char *initialize_steps)
 				initGenerateDataClientSide(con);
 				break;
 			case 'G':
+			case 'i':
+			case 'I':
 				op = "server-side generate";
 				initGenerateDataServerSide(con);
 				break;
-- 
2.43.0


From 5e1827b889b283f50299ce6ab1a73f9f55a4a84f Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Mon, 10 Nov 2025 20:00:56 +0700
Subject: [PATCH 05/12] Fixing typo in query

---
 src/bin/pgbench/pgbench.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 65d77cdefea..03e37df4434 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -5241,7 +5241,8 @@ generateDataInsertSingleTXN(PGconn *con)
 	printfPQExpBuffer(&sql,
 					  "insert into pgbench_branches(bid, bbalance) "
 					  "select bid, 0 "
-					  "from generate_series(1, %d)", scale * nbranches);
+					  "from generate_series(1, %d) as bid",
+					  scale * nbranches);
 	executeStatement(con, sql.data);
 
 	printfPQExpBuffer(&sql,
-- 
2.43.0


From 7ca86521fda6929b8e0de3fc77dcbb8984009c88 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Tue, 11 Nov 2025 19:39:45 +0700
Subject: [PATCH 06/12] Adding support for COPY BINARY mode

---
 src/bin/pgbench/pgbench.c | 393 ++++++++++++++++++++++++++++++++++++--
 1 file changed, 381 insertions(+), 12 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 03e37df4434..71aa1d9479f 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -161,7 +161,7 @@ typedef struct socket_set
  * some configurable parameters */
 
 #define DEFAULT_INIT_STEPS "dtgvp"	/* default -I setting */
-#define ALL_INIT_STEPS "dtgGiIvpf"	/* all possible steps */
+#define ALL_INIT_STEPS "dtgCGiIvpf"	/* all possible steps */
 
 #define LOG_STEP_SECONDS	5	/* seconds between log messages */
 #define DEFAULT_NXACTS	10		/* default nxacts */
@@ -176,6 +176,8 @@ typedef struct socket_set
 /* 'one transaction per scale' server-side methods */
 #define GEN_TYPE_INSERT_SERIES		'i'	/* use INSERT .. SELECT generate_series to generate data */
 #define GEN_TYPE_INSERT_UNNEST  	'I'	/* use INSERT .. SELECT unnest to generate data */
+#define GEN_TYPE_COPY_ORIGINAL		'g' /* use COPY .. FROM STDIN .. TEXT to generate data */
+#define GEN_TYPE_COPY_BINARY		'C' /* use COPY .. FROM STDIN .. BINARY to generate data */
 
 static int	nxacts = 0;			/* number of transactions per client */
 static int	duration = 0;		/* duration in seconds */
@@ -188,10 +190,17 @@ static int64 end_time = 0;		/* when to stop in micro seconds, under -T */
 static int	scale = 1;
 
 /*
- *
+ * mode of data generation to use
  */
 static char	data_generation_type = '?';
 
+/*
+ * COPY FROM BINARY execution buffer
+ */
+#define BIN_COPY_BUF_SIZE	102400				/* maximum buffer size for COPY FROM BINARY */
+static char		*bin_copy_buffer = NULL;		/* buffer for COPY FROM BINARY */
+static int32_t	 bin_copy_buffer_length = 0;	/* current buffer size */
+
 /*
  * fillfactor. for example, fillfactor = 90 will use only 90 percent
  * space during inserts and leave 10 percent free.
@@ -861,7 +870,8 @@ static int	wait_on_socket_set(socket_set *sa, int64 usecs);
 static bool socket_has_input(socket_set *sa, int fd, int idx);
 
 /* callback used to build rows for COPY during data loading */
-typedef void (*initRowMethod) (PQExpBufferData *sql, int64 curr);
+typedef void (*initRowMethod)		(PQExpBufferData *sql, int64 curr);
+typedef void (*initRowMethodBin)	(PGconn *con, PGresult *res, int64_t curr, int32_t parent);
 
 /* callback functions for our flex lexer */
 static const PsqlScanCallbacks pgbench_callbacks = {
@@ -925,6 +935,7 @@ usage(void)
 		   "                           d: drop any existing pgbench tables\n"
 		   "                           t: create the tables used by the standard pgbench scenario\n"
 		   "                           g: generate data, client-side\n"
+		   "                           C:   client-side (single TNX) COPY .. FROM STDIN .. BINARY\n"
 		   "                           G: generate data, server-side in single transaction\n"
 		   "                           i:   server-side (multiple TXNs) INSERT .. SELECT generate_series\n"
 		   "                           I:   server-side (multiple TXNs) INSERT .. SELECT unnest\n"
@@ -5191,9 +5202,9 @@ initPopulateTable(PGconn *con, const char *table, int64 base,
  * a blank-padded string in pgbench_accounts.
  */
 static void
-initGenerateDataClientSide(PGconn *con)
+initGenerateDataClientSideText(PGconn *con)
 {
-	fprintf(stderr, "generating data (client-side)...\n");
+	fprintf(stderr, "TEXT mode...\n");
 
 	/*
 	 * we do all of this in one transaction to enable the backend's
@@ -5209,12 +5220,373 @@ initGenerateDataClientSide(PGconn *con)
 	 * already exist
 	 */
 	initPopulateTable(con, "pgbench_branches", nbranches, initBranch);
-	initPopulateTable(con, "pgbench_tellers", ntellers, initTeller);
+	initPopulateTable(con, "pgbench_tellers",  ntellers,  initTeller);
 	initPopulateTable(con, "pgbench_accounts", naccounts, initAccount);
 
 	executeStatement(con, "commit");
 }
 
+
+/*
+ * Dumps binary buffer to file (purely for debugging)
+ */
+static void
+dumpBufferToFile(char *filename)
+{
+	FILE *file_ptr;
+	size_t bytes_written;
+
+	file_ptr = fopen(filename, "wb");
+	if (file_ptr == NULL)
+	{
+		fprintf(stderr, "Error opening file %s\n", filename);
+		return; // EXIT_FAILURE;
+	}
+
+	bytes_written = fwrite(bin_copy_buffer, 1, bin_copy_buffer_length, file_ptr);
+
+	if (bytes_written != bin_copy_buffer_length)
+	{
+		fprintf(stderr, "Error writing to file or incomplete write\n");
+		fclose(file_ptr);
+		return; // EXIT_FAILURE;
+	}
+
+	fclose(file_ptr);
+}
+
+/*
+ * Save char data to buffer
+ */
+static void
+bufferCharData(char *src, int32_t len)
+{
+	memcpy((char *) bin_copy_buffer + bin_copy_buffer_length, (char *) src, len);
+	bin_copy_buffer_length += len;
+}
+
+/*
+ * Converts platform byte order into network byte order
+ * SPARC doesn't reqire that
+ */
+static void
+bufferData(void *src, int32_t len)
+{
+#ifdef __sparc__
+	bufferCharData(src, len);
+#else
+	if (len == 1)
+		bufferCharData(src, len);
+	else
+		for (int32_t i = 0; i < len; i++)
+		{
+			((char *) bin_copy_buffer + bin_copy_buffer_length)[i] =
+				((char *) src)[len - i - 1];
+		}
+
+	bin_copy_buffer_length += len;
+#endif
+}
+
+/*
+ * adds column counter
+ */
+static void
+addColumnCounter(int16_t n)
+{
+	bufferData((void *) &n, sizeof(n));
+}
+
+/*
+ * adds column with NULL value
+ */
+static void
+addNullColumn()
+{
+	int32_t null = -1;
+	bufferData((void *) &null, sizeof(null));
+}
+
+/*
+ * adds column with int8 value
+ */
+static void
+addInt8Column(int8_t value)
+{
+	int8_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with int16 value
+ */
+static void
+addInt16Column(int16_t value)
+{
+	int16_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with inti32 value
+ */
+static void
+addInt32Column(int32_t value)
+{
+	int32_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with inti64 value
+ */
+static void
+addInt64Column(int64_t value)
+{
+	int64_t	data = value;
+	int32_t	size = sizeof(data);
+	bufferData((void *) &size, sizeof(size));
+	bufferData((void *) &data, sizeof(data));
+}
+
+/*
+ * adds column with char value
+ */
+static void
+addCharColumn(char *value)
+{
+	int32_t	size = strlen(value);
+	bufferData((void *) &size, sizeof(size));
+	bufferCharData(value, size);
+}
+
+/*
+ * Starts communication with server for COPY FROM BINARY statement
+ */
+static void
+sendBinaryCopyHeader(PGconn *con)
+{
+	char header[] = {'P','G','C','O','P','Y','\n','\377','\r','\n','\0',
+					 '\0','\0','\0','\0',
+					 '\0','\0','\0','\0' };
+
+	PQputCopyData(con, header, 19);
+}
+
+/*
+ * Finishes communication with server for COPY FROM BINARY statement
+ */
+static void
+sendBinaryCopyTrailer(PGconn *con)
+{
+	static char trailer[] = { 0xFF, 0xFF };
+
+	PQputCopyData(con, trailer, 2);
+}
+
+/*
+ * Flashes current buffer over network if needed
+ */
+static void
+flushBuffer(PGconn *con, PGresult *res, int16_t row_len)
+{
+	if (bin_copy_buffer_length + row_len > BIN_COPY_BUF_SIZE)
+	{
+		/* flush current buffer */
+		if (PQresultStatus(res) == PGRES_COPY_IN)
+			PQputCopyData(con, (char *) bin_copy_buffer, bin_copy_buffer_length);
+		bin_copy_buffer_length = 0;
+	}
+}
+
+/*
+ * Sends current branch row to buffer
+ */
+static void
+initBranchBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len =  35 + 2 + 4*3; /* max row size is 32 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(2);
+
+	addInt32Column(curr + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Sends current teller row to buffer
+ */
+static void
+initTellerBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len =  40 + 2 + 4*4; /* max row size is 40 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(3);
+
+	addInt32Column(curr + 1);
+	addInt32Column(curr / parent + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Sends current account row to buffer
+ */
+static void
+initAccountBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
+{
+	/*
+	 * Each row has following extra bytes:
+	 * - 2 bytes for number of columns
+	 * - 4 bytes as length for each column
+	 */
+	int16_t	max_row_len = 250 + 2 + 4*4; /* max row size is 250 for int64 */
+
+	flushBuffer(con, res, max_row_len);
+
+	addColumnCounter(3);
+
+	if (scale <= SCALE_32BIT_THRESHOLD)
+		addInt32Column(curr + 1);
+	else
+		addInt64Column(curr);
+
+	addInt32Column(curr / parent + 1);
+	addInt32Column(0);
+}
+
+/*
+ * Universal wrapper for sending data in binary format
+ */
+static void
+initPopulateTableBinary(PGconn *con, char *table, char *columns,
+						int64_t base, initRowMethodBin init_row)
+{
+	int			 n;
+	PGresult	*res;
+	char		 copy_statement[256];
+	const char	*copy_statement_fmt = "copy %s (%s) from stdin (format binary)";
+	int64_t		 total = base * scale;
+
+	bin_copy_buffer_length = 0;
+
+	/* Use COPY with FREEZE on v14 and later for all ordinary tables */
+	if ((PQserverVersion(con) >= 140000) &&
+		get_table_relkind(con, table) == RELKIND_RELATION)
+		copy_statement_fmt = "copy %s (%s) from stdin with (format binary, freeze on)";
+
+	n = pg_snprintf(copy_statement, sizeof(copy_statement), copy_statement_fmt, table, columns);
+	if (n >= sizeof(copy_statement))
+		pg_fatal("invalid buffer size: must be at least %d characters long", n);
+	else if (n == -1)
+		pg_fatal("invalid format string");
+
+	res = PQexec(con, copy_statement);
+
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pg_fatal("unexpected copy in result: %s", PQerrorMessage(con));
+	PQclear(res);
+
+
+	sendBinaryCopyHeader(con);
+
+	for (int64_t i = 0; i < total; i++)
+	{
+		init_row(con, res, i, base);
+	}
+
+	if (PQresultStatus(res) == PGRES_COPY_IN)
+		PQputCopyData(con, (char *) bin_copy_buffer, bin_copy_buffer_length);
+	else
+		fprintf(stderr, "Unexpected mode %d instead of %d\n", PQresultStatus(res), PGRES_COPY_IN);
+
+	sendBinaryCopyTrailer(con);
+
+	if (PQresultStatus(res) == PGRES_COPY_IN)
+	{
+		if (PQputCopyEnd(con, NULL) == 1) /* success */
+		{
+			res = PQgetResult(con);
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				fprintf(stderr, "Error: %s\n", PQerrorMessage(con));
+			PQclear(res);
+		}
+		else
+			fprintf(stderr, "Error: %s\n", PQerrorMessage(con));
+	}
+}
+
+/*
+ * Wrapper for binary data load
+ */
+static void
+initGenerateDataClientSideBinary(PGconn *con)
+{
+
+	fprintf(stderr, "BINARY mode...\n");
+
+	bin_copy_buffer = pg_malloc(BIN_COPY_BUF_SIZE);
+	bin_copy_buffer_length = 0;
+
+	/*
+	 * we do all of this in one transaction to enable the backend's
+	 * data-loading optimizations
+	 */
+	executeStatement(con, "begin");
+
+	/* truncate away any old data */
+	initTruncateTables(con);
+
+	initPopulateTableBinary(con, "pgbench_branches", "bid, bbalance",
+							nbranches, initBranchBinary);
+	initPopulateTableBinary(con, "pgbench_tellers",  "tid, bid, tbalance",
+							ntellers,  initTellerBinary);
+	initPopulateTableBinary(con, "pgbench_accounts", "aid, bid, abalance",
+							naccounts, initAccountBinary);
+
+	executeStatement(con, "commit");
+
+	pg_free(bin_copy_buffer);
+}
+
+/*
+ * Fill the standard tables with some data generated and sent from the client.
+ */
+static void
+initGenerateDataClientSide(PGconn *con)
+{
+	fprintf(stderr, "generating data (client-side) in ");
+
+	switch (data_generation_type)
+	{
+		case GEN_TYPE_COPY_ORIGINAL:
+			initGenerateDataClientSideText(con);
+			break;
+		case GEN_TYPE_COPY_BINARY:
+			initGenerateDataClientSideBinary(con);
+			break;
+	}
+}
+
 /*
  * Generating data via INSERT .. SELECT .. FROM generate_series
  * whole dataset in single transaction
@@ -5500,14 +5872,10 @@ checkInitSteps(const char *initialize_steps)
 
 		switch (*step)
 		{
+			case 'g':
+			case 'C':
 			case 'G':
-				data_init_type++;
-				data_generation_type = *step;
-				break;
 			case 'i':
-				data_init_type++;
-				data_generation_type = *step;
-				break;
 			case 'I':
 				data_init_type++;
 				data_generation_type = *step;
@@ -5555,6 +5923,7 @@ runInitSteps(const char *initialize_steps)
 				initCreateTables(con);
 				break;
 			case 'g':
+			case 'C':
 				op = "client-side generate";
 				initGenerateDataClientSide(con);
 				break;
-- 
2.43.0


From 4aa0ac05765edf6b5f0c13e18ac677287ce78206 Mon Sep 17 00:00:00 2001
From: Fujii Masao <fujii@postgresql.org>
Date: Fri, 14 Nov 2025 22:40:39 +0900
Subject: [PATCH 07/12] pgbench: Fix assertion failure with multiple
 \syncpipeline in pipeline mode.

Previously, when pgbench ran a custom script that triggered retriable errors
(e.g., deadlocks) followed by multiple \syncpipeline commands in pipeline mode,
the following assertion failure could occur:

    Assertion failed: (res == ((void*)0)), function discardUntilSync, file pgbench.c, line 3594.

The issue was that discardUntilSync() assumed a pipeline sync result
(PGRES_PIPELINE_SYNC) would always be followed by either another sync result
or NULL. This assumption was incorrect: when multiple sync requests were sent,
a sync result could instead be followed by another result type. In such cases,
discardUntilSync() mishandled the results, leading to the assertion failure.

This commit fixes the issue by making discardUntilSync() correctly handle cases
where a pipeline sync result is followed by other result types. It now continues
discarding results until another pipeline sync followed by NULL is reached.

Backpatched to v17, where support for \syncpipeline command in pgbench was
introduced.

Author: Yugo Nagata <nagata@sraoss.co.jp>
Reviewed-by: Chao Li <lic@highgo.com>
Reviewed-by: Fujii Masao <masao.fujii@gmail.com>
Discussion: https://postgr.es/m/20251111105037.f3fc554616bc19891f926c5b@sraoss.co.jp
Backpatch-through: 17
---
 src/bin/pgbench/pgbench.c | 39 ++++++++++++++++++++++++++++-----------
 1 file changed, 28 insertions(+), 11 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index d8764ba6fe0..a425176ecdc 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -3563,14 +3563,18 @@ doRetry(CState *st, pg_time_usec_t *now)
 }
 
 /*
- * Read results and discard it until a sync point.
+ * Read and discard results until the last sync point.
  */
 static int
 discardUntilSync(CState *st)
 {
 	bool		received_sync = false;
 
-	/* send a sync */
+	/*
+	 * Send a Sync message to ensure at least one PGRES_PIPELINE_SYNC is
+	 * received and to avoid an infinite loop, since all earlier ones may have
+	 * already been received.
+	 */
 	if (!PQpipelineSync(st->con))
 	{
 		pg_log_error("client %d aborted: failed to send a pipeline sync",
@@ -3578,29 +3582,42 @@ discardUntilSync(CState *st)
 		return 0;
 	}
 
-	/* receive PGRES_PIPELINE_SYNC and null following it */
+	/*
+	 * Continue reading results until the last sync point, i.e., until
+	 * reaching null just after PGRES_PIPELINE_SYNC.
+	 */
 	for (;;)
 	{
 		PGresult   *res = PQgetResult(st->con);
 
+		if (PQstatus(st->con) == CONNECTION_BAD)
+		{
+			pg_log_error("client %d aborted while rolling back the transaction after an error; perhaps the backend died while processing",
+						 st->id);
+			PQclear(res);
+			return 0;
+		}
+
 		if (PQresultStatus(res) == PGRES_PIPELINE_SYNC)
 			received_sync = true;
-		else if (received_sync)
+		else if (received_sync && res == NULL)
 		{
-			/*
-			 * PGRES_PIPELINE_SYNC must be followed by another
-			 * PGRES_PIPELINE_SYNC or NULL; otherwise, assert failure.
-			 */
-			Assert(res == NULL);
-
 			/*
 			 * Reset ongoing sync count to 0 since all PGRES_PIPELINE_SYNC
 			 * results have been discarded.
 			 */
 			st->num_syncs = 0;
-			PQclear(res);
 			break;
 		}
+		else
+		{
+			/*
+			 * If a PGRES_PIPELINE_SYNC is followed by something other than
+			 * PGRES_PIPELINE_SYNC or NULL, another PGRES_PIPELINE_SYNC will
+			 * appear later. Reset received_sync to false to wait for it.
+			 */
+			received_sync = false;
+		}
 		PQclear(res);
 	}
 
-- 
2.43.0


From 9c4f19055597e9adb25e65c2aa8bedf20a09e13d Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Fri, 21 Nov 2025 19:05:58 +0700
Subject: [PATCH 08/12] Setting empty string as default value in filler column

---
 src/bin/pgbench/pgbench.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 967f6ce6984..03b5e5c28f0 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -4985,26 +4985,26 @@ initCreateTables(PGconn *con)
 	static const struct ddlinfo DDLs[] = {
 		{
 			"pgbench_history",
-			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22)",
-			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22)",
+			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22) default ''",
+			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22) default ''",
 			0
 		},
 		{
 			"pgbench_tellers",
-			"tid int not null,bid int,tbalance int,filler char(84)",
-			"tid int not null,bid int,tbalance int,filler char(84)",
+			"tid int not null,bid int,tbalance int,filler char(84) default ''",
+			"tid int not null,bid int,tbalance int,filler char(84) default ''",
 			1
 		},
 		{
 			"pgbench_accounts",
-			"aid    int not null,bid int,abalance int,filler char(84)",
-			"aid bigint not null,bid int,abalance int,filler char(84)",
+			"aid    int not null,bid int,abalance int,filler char(84) default ''",
+			"aid bigint not null,bid int,abalance int,filler char(84) default ''",
 			1
 		},
 		{
 			"pgbench_branches",
-			"bid int not null,bbalance int,filler char(88)",
-			"bid int not null,bbalance int,filler char(88)",
+			"bid int not null,bbalance int,filler char(88) default ''",
+			"bid int not null,bbalance int,filler char(88) default ''",
 			1
 		}
 	};
-- 
2.43.0


From 2aabaa52dffdb78fbefaef95163881c15e18ef29 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Fri, 21 Nov 2025 15:03:11 +0200
Subject: [PATCH 09/12] Use strtoi64() in pgbench, replacing its open-coded
 implementation

Makes the code a little simpler.

The old implementation accepted trailing whitespace, but that was
unnecessary. Firstly, its sibling function for parsing decimals,
strtodouble(), does not accept trailing whitespace. Secondly, none of
the callers can pass a string with trailing whitespace to it.

In the passing, check specifically for ERANGE before printing the "out
of range" error. On some systems, strtoul() and strtod() return EINVAL
on an empty or all-spaces string, and "invalid input syntax" is more
appropriate for that than "out of range". For the existing
strtodouble() function this is purely academical because it's never
called with errorOK==false, but let's be tidy. (Perhaps we should
remove the dead codepaths altogether, but I'll leave that for another
day.)

Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Yuefei Shi <shiyuefei1004@gmail.com>
Reviewed-by: Neil Chen <carpenter.nail.cz@gmail.com>
Discussion: https://www.postgresql.org/message-id/861dd5bd-f2c9-4ff5-8aa0-f82bdb75ec1f@iki.fi
---
 src/bin/pgbench/pgbench.c | 83 +++++++++------------------------------
 1 file changed, 19 insertions(+), 64 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index a425176ecdc..68774a59efd 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -982,13 +982,17 @@ usage(void)
 		   progname, progname, PACKAGE_BUGREPORT, PACKAGE_NAME, PACKAGE_URL);
 }
 
-/* return whether str matches "^\s*[-+]?[0-9]+$" */
+/*
+ * Return whether str matches "^\s*[-+]?[0-9]+$"
+ *
+ * This should agree with strtoint64() on what's accepted, ignoring overflows.
+ */
 static bool
 is_an_int(const char *str)
 {
 	const char *ptr = str;
 
-	/* skip leading spaces; cast is consistent with strtoint64 */
+	/* skip leading spaces */
 	while (*ptr && isspace((unsigned char) *ptr))
 		ptr++;
 
@@ -1012,9 +1016,6 @@ is_an_int(const char *str)
 /*
  * strtoint64 -- convert a string to 64-bit integer
  *
- * This function is a slightly modified version of pg_strtoint64() from
- * src/backend/utils/adt/numutils.c.
- *
  * The function returns whether the conversion worked, and if so
  * "*result" is set to the result.
  *
@@ -1023,71 +1024,25 @@ is_an_int(const char *str)
 bool
 strtoint64(const char *str, bool errorOK, int64 *result)
 {
-	const char *ptr = str;
-	int64		tmp = 0;
-	bool		neg = false;
-
-	/*
-	 * Do our own scan, rather than relying on sscanf which might be broken
-	 * for long long.
-	 *
-	 * As INT64_MIN can't be stored as a positive 64 bit integer, accumulate
-	 * value as a negative number.
-	 */
-
-	/* skip leading spaces */
-	while (*ptr && isspace((unsigned char) *ptr))
-		ptr++;
-
-	/* handle sign */
-	if (*ptr == '-')
-	{
-		ptr++;
-		neg = true;
-	}
-	else if (*ptr == '+')
-		ptr++;
+	char	   *end;
 
-	/* require at least one digit */
-	if (unlikely(!isdigit((unsigned char) *ptr)))
-		goto invalid_syntax;
+	errno = 0;
+	*result = strtoi64(str, &end, 10);
 
-	/* process digits */
-	while (*ptr && isdigit((unsigned char) *ptr))
+	if (unlikely(errno == ERANGE))
 	{
-		int8		digit = (*ptr++ - '0');
-
-		if (unlikely(pg_mul_s64_overflow(tmp, 10, &tmp)) ||
-			unlikely(pg_sub_s64_overflow(tmp, digit, &tmp)))
-			goto out_of_range;
+		if (!errorOK)
+			pg_log_error("value \"%s\" is out of range for type bigint", str);
+		return false;
 	}
 
-	/* allow trailing whitespace, but not other trailing chars */
-	while (*ptr != '\0' && isspace((unsigned char) *ptr))
-		ptr++;
-
-	if (unlikely(*ptr != '\0'))
-		goto invalid_syntax;
-
-	if (!neg)
+	if (unlikely(errno != 0 || end == str || *end != '\0'))
 	{
-		if (unlikely(tmp == PG_INT64_MIN))
-			goto out_of_range;
-		tmp = -tmp;
+		if (!errorOK)
+			pg_log_error("invalid input syntax for type bigint: \"%s\"", str);
+		return false;
 	}
-
-	*result = tmp;
 	return true;
-
-out_of_range:
-	if (!errorOK)
-		pg_log_error("value \"%s\" is out of range for type bigint", str);
-	return false;
-
-invalid_syntax:
-	if (!errorOK)
-		pg_log_error("invalid input syntax for type bigint: \"%s\"", str);
-	return false;
 }
 
 /* convert string to double, detecting overflows/underflows */
@@ -1099,14 +1054,14 @@ strtodouble(const char *str, bool errorOK, double *dv)
 	errno = 0;
 	*dv = strtod(str, &end);
 
-	if (unlikely(errno != 0))
+	if (unlikely(errno == ERANGE))
 	{
 		if (!errorOK)
 			pg_log_error("value \"%s\" is out of range for type double", str);
 		return false;
 	}
 
-	if (unlikely(end == str || *end != '\0'))
+	if (unlikely(errno != 0 || end == str || *end != '\0'))
 	{
 		if (!errorOK)
 			pg_log_error("invalid input syntax for type double: \"%s\"", str);
-- 
2.43.0


From dcb85d26f8132eaaf9d096e814b9bda49db7d478 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Fri, 21 Nov 2025 20:06:24 +0700
Subject: [PATCH 10/12] Switching COPY FROM BINARY ti run in multiple
 transactions

---
 src/bin/pgbench/pgbench.c | 27 ++++++++++++++++-----------
 1 file changed, 16 insertions(+), 11 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 03b5e5c28f0..6b89007a63b 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -5496,20 +5496,20 @@ initAccountBinary(PGconn *con, PGresult *res, int64_t curr, int32_t parent)
  */
 static void
 initPopulateTableBinary(PGconn *con, char *table, char *columns,
-						int64_t base, initRowMethodBin init_row)
+						int counter, int64_t base, initRowMethodBin init_row)
 {
 	int			 n;
 	PGresult	*res;
 	char		 copy_statement[256];
 	const char	*copy_statement_fmt = "copy %s (%s) from stdin (format binary)";
-	int64_t		 total = base * scale;
+	int64_t		 start = base * counter;
 
 	bin_copy_buffer_length = 0;
 
 	/* Use COPY with FREEZE on v14 and later for all ordinary tables */
 	if ((PQserverVersion(con) >= 140000) &&
 		get_table_relkind(con, table) == RELKIND_RELATION)
-		copy_statement_fmt = "copy %s (%s) from stdin with (format binary, freeze on)";
+		copy_statement_fmt = "copy %s (%s) from stdin with (format binary)";
 
 	n = pg_snprintf(copy_statement, sizeof(copy_statement), copy_statement_fmt, table, columns);
 	if (n >= sizeof(copy_statement))
@@ -5526,7 +5526,7 @@ initPopulateTableBinary(PGconn *con, char *table, char *columns,
 
 	sendBinaryCopyHeader(con);
 
-	for (int64_t i = 0; i < total; i++)
+	for (int64_t i = start; i < start + base; i++)
 	{
 		init_row(con, res, i, base);
 	}
@@ -5573,15 +5573,20 @@ initGenerateDataClientSideBinary(PGconn *con)
 	/* truncate away any old data */
 	initTruncateTables(con);
 
-	initPopulateTableBinary(con, "pgbench_branches", "bid, bbalance",
-							nbranches, initBranchBinary);
-	initPopulateTableBinary(con, "pgbench_tellers",  "tid, bid, tbalance",
-							ntellers,  initTellerBinary);
-	initPopulateTableBinary(con, "pgbench_accounts", "aid, bid, abalance",
-							naccounts, initAccountBinary);
-
 	executeStatement(con, "commit");
 
+	for (int i = 0; i < scale; i++)
+	{
+		initPopulateTableBinary(con, "pgbench_branches", "bid, bbalance",
+								i, nbranches, initBranchBinary);
+		initPopulateTableBinary(con, "pgbench_tellers",  "tid, bid, tbalance",
+								i, ntellers,  initTellerBinary);
+		initPopulateTableBinary(con, "pgbench_accounts", "aid, bid, abalance",
+								i, naccounts, initAccountBinary);
+
+		executeStatement(con, "commit");
+	}
+
 	pg_free(bin_copy_buffer);
 }
 
-- 
2.43.0


From b8e28881225234fd00b55235bc60fad2dc60b544 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Sat, 22 Nov 2025 17:06:00 +0700
Subject: [PATCH 11/12] Adding tests for new modes of data generation

---
 src/bin/pgbench/pgbench.c                    | 21 ++++----
 src/bin/pgbench/t/001_pgbench_with_server.pl | 52 +++++++++++++++++---
 2 files changed, 56 insertions(+), 17 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 6b89007a63b..dd4e5d5e056 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -164,7 +164,7 @@ typedef struct socket_set
 #define ALL_INIT_STEPS "dtgCGiIvpf"	/* all possible steps */
 
 #define LOG_STEP_SECONDS	5	/* seconds between log messages */
-#define DEFAULT_NXACTS	10		/* default nxacts */
+#define DEFAULT_NXACTS		10	/* default nxacts */
 
 #define MIN_GAUSSIAN_PARAM		2.0 /* minimum parameter for gauss */
 
@@ -192,7 +192,7 @@ static int	scale = 1;
 /*
  * mode of data generation to use
  */
-static char	data_generation_type = '?';
+static char	data_generation_type = GEN_TYPE_COPY_ORIGINAL;
 
 /*
  * COPY FROM BINARY execution buffer
@@ -4985,26 +4985,26 @@ initCreateTables(PGconn *con)
 	static const struct ddlinfo DDLs[] = {
 		{
 			"pgbench_history",
-			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22) default ''",
-			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22) default ''",
+			"tid int,bid int,aid    int,delta int,mtime timestamp,filler char(22) default '?'",
+			"tid int,bid int,aid bigint,delta int,mtime timestamp,filler char(22) default '?'",
 			0
 		},
 		{
 			"pgbench_tellers",
-			"tid int not null,bid int,tbalance int,filler char(84) default ''",
-			"tid int not null,bid int,tbalance int,filler char(84) default ''",
+			"tid int not null,bid int,tbalance int,filler char(84)",
+			"tid int not null,bid int,tbalance int,filler char(84)",
 			1
 		},
 		{
 			"pgbench_accounts",
-			"aid    int not null,bid int,abalance int,filler char(84) default ''",
-			"aid bigint not null,bid int,abalance int,filler char(84) default ''",
+			"aid    int not null,bid int,abalance int,filler char(84) default '?'",
+			"aid bigint not null,bid int,abalance int,filler char(84) default '?'",
 			1
 		},
 		{
 			"pgbench_branches",
-			"bid int not null,bbalance int,filler char(88) default ''",
-			"bid int not null,bbalance int,filler char(88) default ''",
+			"bid int not null,bbalance int,filler char(88)",
+			"bid int not null,bbalance int,filler char(88)",
 			1
 		}
 	};
@@ -7837,6 +7837,7 @@ main(int argc, char **argv)
 			}
 		}
 
+		checkInitSteps(initialize_steps);
 		runInitSteps(initialize_steps);
 		exit(0);
 	}
diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl
index 581e9af7907..a377048ead1 100644
--- a/src/bin/pgbench/t/001_pgbench_with_server.pl
+++ b/src/bin/pgbench/t/001_pgbench_with_server.pl
@@ -16,25 +16,30 @@ sub check_data_state
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 	my $node = shift;
 	my $type = shift;
+	my $sql_result;
 
-	my $sql_result = $node->safe_psql('postgres',
-		'SELECT count(*) AS null_count FROM pgbench_accounts WHERE filler IS NULL LIMIT 10;'
-	);
-	is($sql_result, '0',
-		"$type: filler column of pgbench_accounts has no NULL data");
 	$sql_result = $node->safe_psql('postgres',
 		'SELECT count(*) AS null_count FROM pgbench_branches WHERE filler IS NULL;'
 	);
 	is($sql_result, '1',
 		"$type: filler column of pgbench_branches has only NULL data");
+
 	$sql_result = $node->safe_psql('postgres',
 		'SELECT count(*) AS null_count FROM pgbench_tellers WHERE filler IS NULL;'
 	);
 	is($sql_result, '10',
 		"$type: filler column of pgbench_tellers has only NULL data");
+
+	$sql_result = $node->safe_psql('postgres',
+		'SELECT count(*) AS null_count FROM pgbench_accounts WHERE filler IS NULL LIMIT 10;'
+	);
+	is($sql_result, '0',
+		"$type: filler column of pgbench_accounts has no NULL data");
+
 	$sql_result = $node->safe_psql('postgres',
 		'SELECT count(*) AS data_count FROM pgbench_history;');
-	is($sql_result, '0', "$type: pgbench_history has no data");
+	is($sql_result, '0',
+		"$type: pgbench_history has no data");
 }
 
 # start a pgbench specific server
@@ -125,7 +130,7 @@ $node->pgbench(
 	'pgbench scale 1 initialization',);
 
 # Check data state, after client-side data generation.
-check_data_state($node, 'client-side');
+check_data_state($node, 'client-side (default options)');
 
 # Again, with all possible options
 $node->pgbench(
@@ -143,6 +148,7 @@ $node->pgbench(
 		qr{done in \d+\.\d\d s }
 	],
 	'pgbench scale 1 initialization');
+check_data_state($node, 'client-side (all options)');
 
 # Test interaction of --init-steps with legacy step-selection options
 $node->pgbench(
@@ -164,6 +170,38 @@ $node->pgbench(
 # Check data state, after server-side data generation.
 check_data_state($node, 'server-side');
 
+# Test server-side generation with UNNEST
+$node->pgbench(
+	'--initialize --init-steps=dtI',
+	0,
+	[qr{^$}],
+	[
+		qr{dropping old tables},
+		qr{creating tables},
+		qr{generating data \(server-side\)},
+		qr{done in \d+\.\d\d s }
+	],
+	'pgbench --init-steps server-side UNNEST');
+
+# Check data state, after server-side data generation.
+check_data_state($node, 'server-side (unnest)');
+
+# Test server-side generation with UNNEST
+$node->pgbench(
+	'--initialize --init-steps=dtC',
+	0,
+	[qr{^$}],
+	[
+		qr{dropping old tables},
+		qr{creating tables},
+		qr{generating data \(client-side\)},
+		qr{done in \d+\.\d\d s }
+	],
+	'pgbench --init-steps client-side BINARY');
+
+# Check data state, after server-side data generation.
+check_data_state($node, 'client-side (binary)');
+
 # Run all builtin scripts, for a few transactions each
 $node->pgbench(
 	'--transactions=5 -Dfoo=bla --client=2 --protocol=simple --builtin=t'
-- 
2.43.0


From b3f2bce34232d299abc7644a8579f0ce49c8c9d6 Mon Sep 17 00:00:00 2001
From: Boris Mironov <boris.mironov@gmail.com>
Date: Sun, 23 Nov 2025 14:05:59 +0700
Subject: [PATCH 12/12] Fixing compiler warnings about unused procedures by
 removing or commenting them out as they might be needed a bit later

---
 src/bin/pgbench/pgbench.c | 31 +++++--------------------------
 1 file changed, 5 insertions(+), 26 deletions(-)

diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index 0a3ba21dcc9..682db61ff61 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -5201,7 +5201,7 @@ initGenerateDataClientSideText(PGconn *con)
 
 /*
  * Dumps binary buffer to file (purely for debugging)
- */
+ *
 static void
 dumpBufferToFile(char *filename)
 {
@@ -5226,6 +5226,7 @@ dumpBufferToFile(char *filename)
 
 	fclose(file_ptr);
 }
+ */
 
 /*
  * Save char data to buffer
@@ -5271,37 +5272,14 @@ addColumnCounter(int16_t n)
 
 /*
  * adds column with NULL value
- */
+ *
 static void
 addNullColumn()
 {
 	int32_t null = -1;
 	bufferData((void *) &null, sizeof(null));
 }
-
-/*
- * adds column with int8 value
  */
-static void
-addInt8Column(int8_t value)
-{
-	int8_t	data = value;
-	int32_t	size = sizeof(data);
-	bufferData((void *) &size, sizeof(size));
-	bufferData((void *) &data, sizeof(data));
-}
-
-/*
- * adds column with int16 value
- */
-static void
-addInt16Column(int16_t value)
-{
-	int16_t	data = value;
-	int32_t	size = sizeof(data);
-	bufferData((void *) &size, sizeof(size));
-	bufferData((void *) &data, sizeof(data));
-}
 
 /*
  * adds column with inti32 value
@@ -5329,7 +5307,7 @@ addInt64Column(int64_t value)
 
 /*
  * adds column with char value
- */
+ *
 static void
 addCharColumn(char *value)
 {
@@ -5337,6 +5315,7 @@ addCharColumn(char *value)
 	bufferData((void *) &size, sizeof(size));
 	bufferCharData(value, size);
 }
+ */
 
 /*
  * Starts communication with server for COPY FROM BINARY statement
-- 
2.43.0