From a70089f8cef227b1fbbc4007acb447106d2f0a04 Mon Sep 17 00:00:00 2001 From: Jakub Vrana Date: Thu, 3 Apr 2025 11:41:43 +0200 Subject: [PATCH] PostgreSQL: Support COPY FROM stdin in SQL query (fix #942) --- CHANGELOG.md | 1 + adminer/drivers/pgsql.inc.php | 38 +++++++++++++++++++++++++++++++++-- adminer/sql.inc.php | 22 +++++++++++--------- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc7ea09f..86d3c6f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Adminer dev - Do not edit NULL values by Modify (bug #967) +- PostgreSQL: Support COPY FROM stdin in SQL query (bug #942) - MySQL: Display number of found rows in group queries (regression from 5.1.1) - non-MySQL: Parse '--' as comment in SQL command (bug SF-842) diff --git a/adminer/drivers/pgsql.inc.php b/adminer/drivers/pgsql.inc.php index 478519ae..30ca0df0 100644 --- a/adminer/drivers/pgsql.inc.php +++ b/adminer/drivers/pgsql.inc.php @@ -6,7 +6,7 @@ add_driver("pgsql", "PostgreSQL"); if (isset($_GET["pgsql"])) { define('Adminer\DRIVER', "pgsql"); if (extension_loaded("pgsql") && $_GET["ext"] != "pdo") { - class Db extends SqlDb { + class PgsqlDb extends SqlDb { public string $extension = "PgSQL"; public int $timeout = 0; private $link, $string, $database = true; @@ -88,6 +88,19 @@ if (isset($_GET["pgsql"])) { function warnings() { return h(pg_last_notice($this->link)); // second parameter is available since PHP 7.1.0 } + + /** Copy from array into a table + * @param list $rows + */ + function copyFrom(string $table, array $rows): bool { + $this->error = ''; + set_error_handler(function ($errno, $error) { + $this->error = (ini_bool('html_errors') ? html_entity_decode($error) : $error); + }); + $return = pg_copy_from($this->link, $table, $rows); + restore_error_handler(); + return $return; + } } class Result { @@ -123,7 +136,7 @@ if (isset($_GET["pgsql"])) { } } elseif (extension_loaded("pdo_pgsql")) { - class Db extends PdoDb { + class PgsqlDb extends PdoDb { public string $extension = "PDO_PgSQL"; public int $timeout = 0; @@ -155,6 +168,12 @@ if (isset($_GET["pgsql"])) { // not implemented in PDO_PgSQL as of PHP 7.2.1 } + function copyFrom(string $table, array $rows): bool { + $return = $this->pdo->pgsqlCopyFromArray($table, $rows); + $this->error = idx($this->pdo->errorInfo(), 2) ?: ''; + return $return; + } + function close() { } } @@ -163,6 +182,21 @@ if (isset($_GET["pgsql"])) { + if (class_exists('Adminer\PgsqlDb')) { + class Db extends PgsqlDb { + function multi_query(string $query) { + if (preg_match('~\bCOPY\s+(.+?)\s+FROM\s+stdin;\n?(.*)\n\\\\\.$~is', str_replace("\r\n", "\n", $query), $match)) { // no ^ to allow leading comments + $rows = explode("\n", $match[2]); + $this->affected_rows = count($rows); + return $this->copyFrom($match[1], $rows); + } + return parent::multi_query($query); + } + } + } + + + class Driver extends SqlDriver { static array $extensions = array("PgSQL", "PDO_PgSQL"); static string $jush = "pgsql"; diff --git a/adminer/sql.inc.php b/adminer/sql.inc.php index f0833d7f..ae2ff7ee 100644 --- a/adminer/sql.inc.php +++ b/adminer/sql.inc.php @@ -72,10 +72,13 @@ if (!$error && $_POST) { while ($query != "") { if (!$offset && preg_match("~^$space*+DELIMITER\\s+(\\S+)~i", $query, $match)) { - $delimiter = $match[1]; + $delimiter = preg_quote($match[1]); $query = substr($query, strlen($match[0])); + } elseif (!$offset && JUSH == 'pgsql' && preg_match("~^($space*+COPY\\s+)[^;]+\\s+FROM\\s+stdin;~i", $query, $match)) { + $delimiter = "\n\\\\\\.\r?\n"; + $offset = strlen($match[0]); } else { - preg_match('(' . preg_quote($delimiter) . "\\s*|$parse)", $query, $match, PREG_OFFSET_CAPTURE, $offset); // should always match + preg_match("($delimiter\\s*|$parse)", $query, $match, PREG_OFFSET_CAPTURE, $offset); // always matches list($found, $pos) = $match[0]; if (!$found && $fp && !feof($fp)) { $query .= fread($fp, 1e5); @@ -85,14 +88,15 @@ if (!$error && $_POST) { } $offset = $pos + strlen($found); - if ($found && rtrim($found) != $delimiter) { // find matching quote or comment end + if ($found && !preg_match("(^$delimiter)", $found)) { // find matching quote or comment end $c_style_escapes = driver()->hasCStyleEscapes() || (JUSH == "pgsql" && ($pos > 0 && strtolower($query[$pos - 1]) == "e")); - $pattern = ($found == '/*' ? '\*/' - : ($found == '[' ? ']' - : (preg_match('~^-- |^#~', $found) ? "\n" - : preg_quote($found) . ($c_style_escapes ? "|\\\\." : ""))) - ); + $pattern = + ($found == '/*' ? '\*/' : + ($found == '[' ? ']' : + (preg_match('~^-- |^#~', $found) ? "\n" : + preg_quote($found) . ($c_style_escapes ? '|\\\\.' : '')))) + ; while (preg_match("($pattern|\$)s", $query, $match, PREG_OFFSET_CAPTURE, $offset)) { $s = $match[0][0]; @@ -108,7 +112,7 @@ if (!$error && $_POST) { } else { // end of a query $empty = false; - $q = substr($query, 0, $pos); + $q = substr($query, 0, $pos + ($delimiter[0] == "\n" ? 3 : 0)); // 3 - pass "\n\\." to PostgreSQL COPY $commands++; $print = "
" . adminer()->sqlCommandQuery($q) . "
\n"; if (JUSH == "sqlite" && preg_match("~^$space*+ATTACH\\b~i", $q, $match)) {