From 59c1a6059b2fca4a77e7820d6a6fe4fda97061af Mon Sep 17 00:00:00 2001 From: Shinya Kato Date: Tue, 18 Aug 2026 22:37:47 +0900 Subject: [PATCH v2 1/2] Make pgbench \gset and \aset store SQL NULL as the null value Previously, readCommandResponse() stored every column with PQgetvalue(), which returns an empty string for SQL NULL. An empty string is not a valid value in the pgbench expression language, so the first expression use of such a variable aborted the client, and neither --max-tries nor --continue-on-error recovered from it. An empty string returned by the server was also indistinguishable from a NULL. Check PQgetisnull() on the row being stored and assign the null value for NULL columns, exactly as \set varname NULL does, so the variable can be tested with IS NULL. Empty strings are still stored as empty strings. Scripts that captured a NULL and interpolated it textually now get NULL instead of an empty string. A NULL-valued variable is still bound as the string "NULL" in the extended and prepared query modes, just as one created by \set is. Author: Shinya Kato Reviewed-by: Kyotaro Horiguchi Discussion: https://postgr.es/m/CAOzEurT9J9QMmpJt_8pWZCziNYCUHOD=c-hoccOVYJLc_VxVRg@mail.gmail.com --- src/bin/pgbench/pgbench.c | 19 +++++- src/bin/pgbench/t/001_pgbench_with_server.pl | 67 ++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c index 5862758427f..bfd33383f7d 100644 --- a/src/bin/pgbench/pgbench.c +++ b/src/bin/pgbench/pgbench.c @@ -3319,6 +3319,7 @@ readCommandResponse(CState *st, MetaCommand meta, char *varprefix) if ((is_last && meta == META_GSET) || meta == META_ASET) { int ntuples = PQntuples(res); + const char *context = meta == META_ASET ? "aset" : "gset"; if (meta == META_GSET && ntuples != 1) { @@ -3338,14 +3339,26 @@ readCommandResponse(CState *st, MetaCommand meta, char *varprefix) for (int fld = 0; fld < PQnfields(res); fld++) { char *varname = PQfname(res, fld); + bool ok; /* allocate varname only if necessary, freed below */ if (*varprefix != '\0') varname = psprintf("%s%s", varprefix, varname); - /* store last row result as a string */ - if (!putVariable(&st->variables, meta == META_ASET ? "aset" : "gset", varname, - PQgetvalue(res, ntuples - 1, fld))) + /* store last row result, SQL NULL as null value */ + if (PQgetisnull(res, ntuples - 1, fld)) + { + PgBenchValue nullval; + + setNullValue(&nullval); + ok = putVariableValue(&st->variables, context, + varname, &nullval); + } + else + ok = putVariable(&st->variables, context, varname, + PQgetvalue(res, ntuples - 1, fld)); + + if (!ok) { /* internal error */ pg_log_error("client %d script %d command %d query %d: error storing into variable %s", diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl index bb12ade59f5..8913dc62c42 100644 --- a/src/bin/pgbench/t/001_pgbench_with_server.pl +++ b/src/bin/pgbench/t/001_pgbench_with_server.pl @@ -850,6 +850,73 @@ SELECT 5432 AS fail UNION SELECT 5433 ORDER BY 1 \gset } }); +# \gset stores a SQL NULL as the null value, which expressions can use. +$node->pgbench( + '-t 1', 0, + [ qr{type: .*/001_pgbench_gset_null}, qr{processed: 1/1} ], + [ qr{command=2.: null\b}, qr{command=3.: boolean true\b} ], + 'pgbench gset command with NULL', + { + '001_pgbench_gset_null' => q{-- NULL is stored as the null value +SELECT NULL AS nv \gset +\set i debug(:nv) +\set i debug(:nv IS NULL) +} + }); + +# NULL and empty string captured by \gset stay distinct when interpolated +$node->safe_psql('postgres', + 'CREATE UNLOGGED TABLE gset_null_tab(id INTEGER, t TEXT);'); + +$node->pgbench( + '-t 1', 0, + [ qr{type: .*/001_pgbench_gset_null_interp}, qr{processed: 1/1} ], + [], + 'pgbench gset NULL interpolation', + { + '001_pgbench_gset_null_interp' => q{-- interpolate NULL and '' +SELECT NULL AS nv, ''::text AS es \gset +INSERT INTO gset_null_tab VALUES (1, :nv); +INSERT INTO gset_null_tab VALUES (2, ':es'); +} + }); + +is( $node->safe_psql( + 'postgres', 'SELECT t IS NULL FROM gset_null_tab WHERE id = 1;'), + 't', + 'gset NULL interpolates as SQL NULL'); +is( $node->safe_psql( + 'postgres', "SELECT t = '' FROM gset_null_tab WHERE id = 2;"), + 't', + 'gset empty string stays empty'); + +# In the extended and prepared query modes a NULL-valued variable is bound as +# the string "NULL", not as an SQL null parameter. +for my $mode ('extended', 'prepared') +{ + $node->pgbench( + "-t 1 -M $mode", + 0, + [ qr{type: .*/001_pgbench_gset_null_param_$mode}, qr{processed: 1/1} ], + [], + "pgbench gset NULL as query parameter ($mode)", + { + "001_pgbench_gset_null_param_$mode" => + q{-- NULL bound as a query parameter +SELECT NULL AS nv \gset +INSERT INTO gset_null_tab VALUES (3, :nv); +} + }); +} + +is( $node->safe_psql( + 'postgres', + "SELECT count(*) FROM gset_null_tab WHERE id = 3 AND t = 'NULL';"), + '2', + 'gset NULL is bound as the string NULL in extended and prepared modes'); + +$node->safe_psql('postgres', 'DROP TABLE gset_null_tab;'); + # working \aset # Valid cases. $node->pgbench( -- 2.47.3