Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wucI9-000p2A-1M for pgsql-bugs@arkaria.postgresql.org; Thu, 13 Aug 2026 20:44:45 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wucI7-00G6yE-0C for pgsql-bugs@arkaria.postgresql.org; Thu, 13 Aug 2026 20:44:44 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wucI6-00G6y6-2a for pgsql-bugs@lists.postgresql.org; Thu, 13 Aug 2026 20:44:43 +0000 Received: from sss.pgh.pa.us ([68.162.161.243]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wucI5-00000000V09-3kFQ for pgsql-bugs@lists.postgresql.org; Thu, 13 Aug 2026 20:44:43 +0000 Received: from sss1.sss.pgh.pa.us (localhost [127.0.0.1]) by sss.pgh.pa.us (8.18.1/8.18.1) with ESMTP id 67DKiZEE3598004; Thu, 13 Aug 2026 16:44:35 -0400 From: Tom Lane To: Zsolt Parragi cc: pgsql-bugs@lists.postgresql.org Subject: Re: COPY TO regression with psql -c In-reply-to: References: <2801602.1786557522@sss.pgh.pa.us> Comments: In-reply-to Zsolt Parragi message dated "Wed, 12 Aug 2026 20:53:17 +0100" MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0" Content-ID: <3597940.1786653813.0@sss.pgh.pa.us> Date: Thu, 13 Aug 2026 16:44:35 -0400 Message-ID: <3598003.1786653875@sss.pgh.pa.us> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk ------- =_aaaaaaaaaa0 Content-Type: text/plain; charset="us-ascii" Content-ID: <3597940.1786653813.1@sss.pgh.pa.us> After thinking some more about how to handle cases where we stop with PSCAN_BACKSLASH or PSCAN_INCOMPLETE, I feel that the safest answer is to set num_copy_from_stdin = 0 in those cases. This is consistent with the fact that we know we won't get a PGRES_COPY_IN message, even if there was a valid COPY FROM STDIN in the string before the syntax error. This might prevent us from skipping following data in cases where it'd be best to do that, but here are two arguments against trying to do so: * The ambition of the security patch extended only to handling syntactically-valid cases, which these aren't. Trying to do more leads into a guessing game, eg should we skip data after "COPY mytable FRPM STDIN"? * Not trying to skip data ensures that the behavior of such cases is the same as it was before the security patch, which seems like the right direction to err in. So v3 attached does it like that. I also simplified the test script. The two-COPY-commands case seems like it covers everything we want to test; the other cases just add cycles and complicate the script. regards, tom lane ------- =_aaaaaaaaaa0 Content-Type: text/x-diff; name*0="v3-0001-psql-count-every-COPY-FROM-STDIN-when-scanning-a-.p"; name*1="atch"; charset="us-ascii" Content-ID: <3597940.1786653813.2@sss.pgh.pa.us> Content-Description: v3-0001-psql-count-every-COPY-FROM-STDIN-when-scanning-a-.patch Content-Transfer-Encoding: quoted-printable =46rom 45fbd29420928f6230762f99e1901b1a45ff1c4b Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 13 Aug 2026 16:26:08 -0400 Subject: [PATCH v3] psql: count every COPY FROM STDIN when scanning a quer= y string. When SendQuery() is not told how many COPY FROM STDIN commands the query string contains (as for -c, \gexec, and \watch), it scans 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, causing failure of cases that used to work. Oversight in commit 3045a25ba. Author: Zsolt Parragi Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CAN4CZFPqa6c+u4uX5jJ8LANHTQ4dxM3m4_8G9WmX_= A4-2wuv2A@mail.gmail.com Backpatch-through: 14 --- src/bin/psql/common.c | 25 +++++++++++++++++++++++-- src/bin/psql/t/001_basic.pl | 30 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 314bf2388ac..f220344daaf 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -1796,15 +1796,36 @@ ExecQueryAndProcessResults(const char *query, PsqlScanState scan_state; PQExpBuffer query_buf; promptStatus_t prompt_tmp; + PsqlScanResult scan_result; = scan_state =3D psql_scan_create(&psqlscan_callbacks); psql_scan_setup(scan_state, query, strlen(query), pset.encoding, standard_strings()); query_buf =3D 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 =3D psql_scan(scan_state, query_buf, &prompt_tmp); + } while (scan_result =3D=3D PSCAN_SEMICOLON); = - num_copy_from_stdin =3D psql_scan_count_copy_from_stdin(scan_state); + /* + * We expect the result now to be PSCAN_EOL. If it is PSCAN_BACKSLASH + * or PSCAN_INCOMPLETE, the server will get a parse error and refuse + * to execute any part of the command string, so don't expect any + * PGRES_COPY_IN results. (This will mean that we don't attempt to + * discard any following data, but this seems consistent with the + * general contract of psql_scan_count_copy_from_stdin, which is that + * it only promises to count syntactically-valid COPY commands.) + */ + if (scan_result =3D=3D PSCAN_EOL) + num_copy_from_stdin =3D psql_scan_count_copy_from_stdin(scan_state); + else + num_copy_from_stdin =3D 0; = destroyPQExpBuffer(query_buf); psql_scan_destroy(scan_state); diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl index 04644f2fdfc..028df33ce8a 100644 --- a/src/bin/psql/t/001_basic.pl +++ b/src/bin/psql/t/001_basic.pl @@ -533,6 +533,36 @@ psql_fails_like( qr/COPY in a pipeline is not supported, aborting connection/, '\copy to in pipeline: fails'); = +# Test execution of COPY FROM STDIN in -c. This case is a bit weird +# because it will read from psql's stdin not from the command source. +# To make it even weirder, try two such commands, to stress psql's logic +# that counts them. Also test both \. and EOF termination. +{ + $node->safe_psql('postgres', 'CREATE TABLE copy_stdin_count (a int)'); + my ($stdin, $stdout, $stderr) =3D ("50\n\\.\n60\n", '', ''); + my $ret =3D IPC::Run::run( + [ + 'psql', '--no-psqlrc', + '--set' =3D> 'ON_ERROR_STOP=3D1', + '--dbname' =3D> $node->connstr('postgres'), + '--command' =3D> + 'COPY copy_stdin_count FROM STDIN; COPY copy_stdin_count FROM STDIN'= , + ], + '<' =3D> \$stdin, + '>' =3D> \$stdout, + '2>' =3D> \$stderr); + + ok($ret, '-c COPY FROM STDIN: psql exits 0'); + unlike( + $stderr, + qr/unexpected COPY_IN result/, + '-c COPY FROM STDIN: unexpected COPY_IN result'); + + my $data =3D $node->safe_psql('postgres', 'SELECT * FROM copy_stdin_coun= t'); + is($data, "50\n60", '-c COPY FROM STDIN: correct data loaded'); +} + +# Test \restrict and \unrestrict. psql_fails_like( $node, qq{\\restrict test -- = 2.52.0 ------- =_aaaaaaaaaa0--