From 1008093a2b76cb57bee12df7223d100bab49eded Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Tue, 11 Aug 2026 18:16:28 +0000 Subject: [PATCH] psql: count every COPY FROM STDIN when scanning a query string When SendQuery() is not told how many COPY FROM STDIN commands the query string contains (num_copy_from_stdin < 0, as for -c, \gexec and forced sends), it scanned the string to count them itself. But it called psql_scan() only once, which stops at the first semicolon, so any COPY FROM STDIN past the first sub-command was not counted. psql then treated the server's COPY_IN response as unexpected and closed the connection with "unexpected COPY_IN result, aborting connection", e.g. for psql -c "SELECT 1; COPY tab FROM STDIN" < data which worked before the counting was added. Loop psql_scan() over the whole string so the count accumulates across all sub-commands. Add a TAP test covering a lone COPY, a COPY after another command, and two COPYs in one string. Oversight in commit 3045a25ba81. --- src/bin/psql/common.c | 12 +++- src/bin/psql/meson.build | 1 + src/bin/psql/t/040_copy_stdin_count.pl | 78 ++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/bin/psql/t/040_copy_stdin_count.pl diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 314bf2388ac..e5a37d7073f 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -1796,13 +1796,23 @@ ExecQueryAndProcessResults(const char *query, PsqlScanState scan_state; PQExpBuffer query_buf; promptStatus_t prompt_tmp; + PsqlScanResult scan_result; scan_state = psql_scan_create(&psqlscan_callbacks); psql_scan_setup(scan_state, query, strlen(query), pset.encoding, standard_strings()); query_buf = createPQExpBuffer(); - (void) psql_scan(scan_state, query_buf, &prompt_tmp); + /* + * A semicolon ends only one sub-command; keep scanning so that COPY + * FROM STDIN commands past the first semicolon are counted too. The + * count accumulates in scan_state across the psql_scan() calls. + */ + do + { + scan_result = psql_scan(scan_state, query_buf, &prompt_tmp); + resetPQExpBuffer(query_buf); + } while (scan_result == PSCAN_SEMICOLON); num_copy_from_stdin = psql_scan_count_copy_from_stdin(scan_state); diff --git a/src/bin/psql/meson.build b/src/bin/psql/meson.build index 922b2845267..d6a671d0017 100644 --- a/src/bin/psql/meson.build +++ b/src/bin/psql/meson.build @@ -78,6 +78,7 @@ tests += { 't/010_tab_completion.pl', 't/020_cancel.pl', 't/030_pager.pl', + 't/040_copy_stdin_count.pl', ], }, } diff --git a/src/bin/psql/t/040_copy_stdin_count.pl b/src/bin/psql/t/040_copy_stdin_count.pl new file mode 100644 index 00000000000..7eee668535e --- /dev/null +++ b/src/bin/psql/t/040_copy_stdin_count.pl @@ -0,0 +1,78 @@ + +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +# Test that psql counts every COPY ... FROM STDIN in a query string when it has +# to scan the string itself (the -c / \gexec / forced-send paths). A COPY that +# is not the first sub-command must still be recognized, otherwise psql treats +# the server's COPY_IN response as unexpected and aborts the connection. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +$node->safe_psql('postgres', 'CREATE TABLE t (a int)'); + +# Run "psql -c $sql" with $stdin fed to psql's stdin (so COPY FROM STDIN reads +# it). Returns (success, stdout, stderr). +sub psql_c_stdin +{ + my ($sql, $stdin) = @_; + my ($stdout, $stderr) = ('', ''); + + my $ret = IPC::Run::run( + [ + 'psql', '-X', '-v' => 'ON_ERROR_STOP=1', + '-d' => $node->connstr('postgres'), + '-c' => $sql + ], + '<' => \$stdin, + '>' => \$stdout, + '2>' => \$stderr); + + return ($ret, $stdout, $stderr); +} + +my @cases = ( + { + name => 'single COPY FROM STDIN', + sql => 'COPY t FROM STDIN', + data => "10\n20\n\\.\n", + rows => 2, + }, + { + name => 'COPY FROM STDIN after another command', + sql => 'SELECT 1; COPY t FROM STDIN', + data => "30\n40\n\\.\n", + rows => 2, + }, + { + name => 'two COPY FROM STDIN in one string', + sql => 'COPY t FROM STDIN; COPY t FROM STDIN', + data => "50\n\\.\n60\n\\.\n", + rows => 2, + }); + +foreach my $c (@cases) +{ + $node->safe_psql('postgres', 'TRUNCATE t'); + + my ($ok, $stdout, $stderr) = psql_c_stdin($c->{sql}, $c->{data}); + + ok($ok, "$c->{name}: psql exits 0"); + unlike($stderr, qr/unexpected COPY_IN result/, + "$c->{name}: connection not aborted"); + + my $count = $node->safe_psql('postgres', 'SELECT count(*) FROM t'); + is($count, $c->{rows}, "$c->{name}: all rows loaded"); +} + +$node->stop; + +done_testing(); -- 2.54.0