From 9e823ccfeadc867cfe5290d5956fc055cc9eeeb0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:08:31 +0000 Subject: [PATCH] feat: export via COPY TO STDOUT instead of temp view (SUPPORT-17025) Co-Authored-By: tereza.skalova --- src/Extractor/CopyAdapter.php | 49 ++++--------------- .../error-invalid-query/expected-stderr | 2 +- .../expected-stdout | 4 +- 3 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/Extractor/CopyAdapter.php b/src/Extractor/CopyAdapter.php index 0b654c24..f03f09b5 100644 --- a/src/Extractor/CopyAdapter.php +++ b/src/Extractor/CopyAdapter.php @@ -98,22 +98,17 @@ public function doExport( $sql = '\encoding UTF8' . PHP_EOL . implode(PHP_EOL, $this->databaseConfig->getInitQueries()) . PHP_EOL; - if ($this->canUserCreateView() && !$this->isTransactionReadOnly()) { - $viewName = uniqid(); - $sql .= 'CREATE TEMP VIEW "' . $viewName . '" AS ' . $trimmedQuery . ';' . PHP_EOL . - sprintf( - "\COPY (%s) TO '%s' WITH CSV DELIMITER ',' FORCE QUOTE *;", - 'SELECT * FROM "' . $viewName . '"', - $csvPath, - ) . PHP_EOL . - 'DROP VIEW "' . $viewName . '";'; - } else { - $sql .= sprintf( - "\COPY (%s) TO '%s' WITH CSV DELIMITER ',' FORCE QUOTE *;", + // Export data using a server-side "COPY ... TO STDOUT" statement redirected to the output + // file with the psql "\o" meta-command. Unlike the client-side "\copy" meta-command, + // "COPY ... TO STDOUT" is a regular SQL statement, so it supports multi-line queries without + // wrapping them in a temporary view. This avoids any DDL (CREATE/DROP TEMP VIEW) on the + // source database during export. + $sql .= sprintf("\o '%s'", $csvPath) . PHP_EOL . + sprintf( + "COPY (%s) TO STDOUT WITH CSV DELIMITER ',' FORCE QUOTE *;", $trimmedQuery, - $csvPath, - ); - } + ) . PHP_EOL . + '\o'; try { $this->runPsqlProcess($sql, null); @@ -203,28 +198,4 @@ private function getLastFetchedValue( return $lastExportedRow[$columnIndex]; } - - protected function canUserCreateView(): bool - { - try { - return $this->runPsqlProcess( - 'SELECT has_schema_privilege(current_schema(), \'CREATE\');', - null, - ) === 't'; - } catch (CopyAdapterException $e) { - throw new CopyAdapterQueryException($e->getMessage(), 0, $e); - } - } - - protected function isTransactionReadOnly(): bool - { - try { - return $this->runPsqlProcess( - 'SHOW transaction_read_only;', - null, - ) === 'on'; - } catch (CopyAdapterException $e) { - throw new CopyAdapterQueryException($e->getMessage(), 0, $e); - } - } } diff --git a/tests/functional/error-invalid-query/expected-stderr b/tests/functional/error-invalid-query/expected-stderr index 5aeffd07..eb354bea 100644 --- a/tests/functional/error-invalid-query/expected-stderr +++ b/tests/functional/error-invalid-query/expected-stderr @@ -1,3 +1,3 @@ -Export by "Copy" adapter failed: ERROR: column "something" does not exist LINE 1: CREATE TEMP VIEW "%x" AS SELECT something, fake;%A +Export by "Copy" adapter failed: ERROR: column "something" does not exist LINE 1: COPY (SELECT something, fake) TO STDOUT WITH CSV DELIMITER '...%A Export by "PDO" adapter failed: [in.c-main.escaping]: DB query failed: SQLSTATE[42703]: Undefined column: 7 ERROR: column "something" does not exist%A Tried 5 times. [in.c-main.escaping]: DB query failed: SQLSTATE[42703]: Undefined column: 7 ERROR: column "something" does not exist %A Tried 5 times. diff --git a/tests/functional/run-action-row-debug-logging/expected-stdout b/tests/functional/run-action-row-debug-logging/expected-stdout index 77c7f625..21749704 100644 --- a/tests/functional/run-action-row-debug-logging/expected-stdout +++ b/tests/functional/run-action-row-debug-logging/expected-stdout @@ -2,7 +2,5 @@ Creating PDO connection to "pgsql:host=%s;port=5432;dbname=postgres;". Exporting "sales" to "in.c-main.escaping". Exporting by "Copy" adapter. -[%s] DEBUG: Running query: "SELECT has_schema_privilege(current_schema(), 'CREATE');". [] [] -[%s] DEBUG: Running query: "SHOW transaction_read_only;". [] [] -[%s] DEBUG: Running query: "\encoding UTF8 CREATE TEMP VIEW "%s" AS SELECT * FROM escaping; \COPY (SELECT * FROM "%s") TO '/tmp/run-%s.%s/out/tables/in.c-main.escaping.csv' WITH CSV DELIMITER ',' FORCE QUOTE *; DROP VIEW "%s";". [] [] +[%s] DEBUG: Running query: "\encoding UTF8 \o '/tmp/run-%s.%s/out/tables/in.c-main.escaping.csv' COPY (SELECT * FROM escaping) TO STDOUT WITH CSV DELIMITER ',' FORCE QUOTE *; \o". [] [] Exported "7" rows to "in.c-main.escaping".