Skip to content

Commit 9f37f5b

Browse files
davecramerclaude
andcommitted
Reject COPY ... FROM STDIN / TO STDOUT with HYC00 instead of poisoning the transaction
COPY ... FROM STDIN and COPY ... TO STDOUT hand the connection over to libpq's COPY streaming sub-protocol, which this driver does not implement. handle_execution_result had no case for the PGRES_COPY_IN/OUT/BOTH result statuses, so a COPY fell into the default branch: we PQclear'd the result and returned SQL_ERROR while the connection was still in copy mode. The next command the driver issued (the per-statement SAVEPOINT teardown) then terminated the copy abnormally ("COPY terminated by new PQexec"), aborting the whole transaction so every following statement failed with 25P02. Add connection_abort_active_copy(): end a copy-in with a non-NULL error string, drain and discard a copy-out, then consume the trailing result(s) so the connection leaves copy mode. When the unwind leaves the transaction aborted, mark it FAILED so connection_handle_statement_error rewinds to the per-statement savepoint and keeps the transaction usable. handle_execution_result now has explicit PGRES_COPY_IN/OUT/BOTH cases returning HYC00 ("optional feature not implemented"), covering both the SQLExecDirect and SQLPrepare/SQLExecute paths. Detection keys off the libpq result status, not the SQL text, so a file-based COPY (COPY ... TO '/path', which returns PGRES_COMMAND_OK) or a column literally named "stdin" is never mistaken for a client-stream COPY. This restores upstream regression parity: commands-test.c gained a COPY section in upstream commit bc439e8. Full suite now 60/60 on PostgreSQL 17 and 20. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9785653 commit 9f37f5b

3 files changed

Lines changed: 90 additions & 0 deletions

File tree

‎src/connection.c‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,3 +758,51 @@ void connection_handle_statement_error(OdbcConnection *connection)
758758
* leave the transaction FAILED for the application to resolve with its own
759759
* SQLEndTran(ROLLBACK). */
760760
}
761+
762+
void connection_abort_active_copy(OdbcConnection *connection, ExecStatusType copy_status)
763+
{
764+
if (!connection || !connection->libpq_connection) {
765+
return;
766+
}
767+
PGconn *libpq_connection = connection->libpq_connection;
768+
769+
/* A COPY ... FROM STDIN leaves libpq waiting to stream CopyData we will
770+
* never produce. End it with a non-NULL error string so the server fails
771+
* the COPY immediately instead of blocking in ClientRead forever. */
772+
if (copy_status == PGRES_COPY_IN || copy_status == PGRES_COPY_BOTH) {
773+
PQputCopyEnd(libpq_connection,
774+
"COPY ... FROM STDIN / TO STDOUT is not supported in ODBC");
775+
}
776+
777+
/* A COPY ... TO STDOUT streams rows to the client. There is no "abort"
778+
* primitive for copy-out, so drain and discard the data until the stream
779+
* ends, which returns the connection to a normal command state. */
780+
if (copy_status == PGRES_COPY_OUT || copy_status == PGRES_COPY_BOTH) {
781+
char *copy_row = NULL;
782+
int copy_bytes;
783+
/* >0 = a row (must be freed), -1 = copy done, -2 = error. */
784+
while ((copy_bytes = PQgetCopyData(libpq_connection, &copy_row, 0)) > 0) {
785+
if (copy_row) {
786+
PQfreemem(copy_row);
787+
copy_row = NULL;
788+
}
789+
}
790+
if (copy_row) {
791+
PQfreemem(copy_row);
792+
}
793+
}
794+
795+
/* Consume the trailing result(s) so the connection leaves copy mode and is
796+
* usable again. Aborting a copy-in makes the server report the COPY as
797+
* failed, which poisons the surrounding transaction — mark it FAILED so the
798+
* caller's statement-error handler rewinds to the per-statement savepoint
799+
* and keeps the transaction (and its earlier work) usable. */
800+
PGresult *trailing_result;
801+
while ((trailing_result = PQgetResult(libpq_connection)) != NULL) {
802+
if (PQresultStatus(trailing_result) == PGRES_FATAL_ERROR &&
803+
connection->transaction_state == TRANSACTION_STATE_ACTIVE) {
804+
connection->transaction_state = TRANSACTION_STATE_FAILED;
805+
}
806+
PQclear(trailing_result);
807+
}
808+
}

‎src/connection.h‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,4 +415,17 @@ void connection_begin_statement_savepoint(OdbcConnection *connection);
415415
*/
416416
void connection_handle_statement_error(OdbcConnection *connection);
417417

418+
/*
419+
* Unwind an in-progress COPY that libpq handed back as PGRES_COPY_IN,
420+
* PGRES_COPY_OUT, or PGRES_COPY_BOTH. This driver does not implement the COPY
421+
* streaming sub-protocol, so a COPY ... FROM STDIN / TO STDOUT must not be left
422+
* mid-stream: doing so would block the backend (copy-in) and cause the next
423+
* command the driver issues to terminate the copy abnormally, poisoning the
424+
* whole transaction. Ends copy-in with an error, drains copy-out, and consumes
425+
* the trailing result(s) so the connection returns to a usable state. When the
426+
* unwound copy left the transaction aborted, the connection is marked FAILED so
427+
* connection_handle_statement_error can rewind to the per-statement savepoint.
428+
*/
429+
void connection_abort_active_copy(OdbcConnection *connection, ExecStatusType copy_status);
430+
418431
#endif /* PSQLODBC2_CONNECTION_H */

‎src/statement.c‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,35 @@ static SQLRETURN handle_execution_result(OdbcStatement *statement, PGresult *res
403403
return SQL_ERROR;
404404
}
405405

406+
case PGRES_COPY_IN:
407+
case PGRES_COPY_OUT:
408+
case PGRES_COPY_BOTH: {
409+
/* COPY ... FROM STDIN / TO STDOUT hands the connection over to libpq's
410+
* COPY streaming sub-protocol, which this driver does not implement.
411+
* If left in copy mode, the server would wait forever for CopyData we
412+
* never send, and the next command the driver issues (e.g. the
413+
* per-statement SAVEPOINT teardown) would terminate the copy abnormally
414+
* and abort the whole transaction. Unwind the copy so the connection is
415+
* usable again, then report it as an unimplemented optional feature.
416+
*
417+
* Detection keys off the libpq result STATUS, not the SQL text, so a
418+
* file-based COPY (COPY ... TO '/path', which returns PGRES_COMMAND_OK)
419+
* or a column literally named "stdin" is never mistaken for a
420+
* client-stream COPY. */
421+
connection_abort_active_copy(statement->parent_connection, status);
422+
PQclear(result);
423+
424+
/* Match the original driver's wording exactly — the regression suite
425+
* compares against "<context>;\n<detail>". */
426+
diagnostics_clear(&statement->diagnostics);
427+
diagnostics_add_record(&statement->diagnostics,
428+
"HYC00", /* Optional feature not implemented */
429+
0,
430+
"Error while executing the query;\n"
431+
"COPY ... FROM STDIN / TO STDOUT is not supported in ODBC");
432+
return SQL_ERROR;
433+
}
434+
406435
default: {
407436
/* Unexpected result status — extract whatever error info is available */
408437
diagnostics_clear(&statement->diagnostics);

0 commit comments

Comments
 (0)