Postgresql: Fix reconnect & transactions, add \n in long queries, add support for VALUES (), fix additional where conditions in DELETE .. USING

master
Vitaliy Filippov 2017-12-13 17:01:25 +03:00
parent 5a45252587
commit 33f9eb64f8
1 changed files with 120 additions and 63 deletions

View File

@ -3,8 +3,8 @@
/**
* PDO/PostgreSQL wrapper with (mostly) DatabaseMySQL interface :)
* Select builder is inspired by MediaWiki's one.
* Version: 2016-09-02
* (c) Vitaliy Filippov, 2015-2016
* Version: 2017-12-13
* (c) Vitaliy Filippov, 2015-2017
*/
if (!defined('MS_HASH'))
@ -38,7 +38,7 @@ if (!interface_exists('Database'))
class DatabasePdoPgsql implements Database
{
var $host, $port, $socket, $username, $password, $dbname;
var $host, $port, $socket, $username, $password, $dbname, $pgbouncer;
var $tableNames = array();
var $init = array();
@ -84,6 +84,7 @@ class DatabasePdoPgsql implements Database
'queryLogFile' => '',
'autoBegin' => false,
'ondestroy' => 'commit',
'pgbouncer' => false,
'init' => array(),
);
$options += $defOpts;
@ -116,7 +117,7 @@ class DatabasePdoPgsql implements Database
$this->link = new PDO($str, $this->username, $this->password, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_EMULATE_PREPARES => !empty($this->pgbouncer),
));
foreach ($this->init as $q)
$this->link->query($q);
@ -167,19 +168,28 @@ class DatabasePdoPgsql implements Database
$this->begin();
$this->queryCount++;
if ($this->queryLogFile)
file_put_contents($this->queryLogFile, date("[Y-m-d H:i:s] ").$sql."\n", FILE_APPEND);
if (strlen($sql) == 5 && strtoupper($sql) == 'BEGIN')
return $this->link->beginTransaction();
elseif (strlen($sql) == 8 && strtoupper($sql) == 'ROLLBACK')
return $this->link->rollBack();
elseif (strlen($sql) == 6 && strtoupper($sql) == 'COMMIT')
return $this->link->commit();
return $this->link->query($sql);
{
if (strlen($sql) <= 100)
$sql = preg_replace("/\n\s*/", ' ', $sql);
$t = substr(floatval(microtime()), 1, 7);
file_put_contents($this->queryLogFile, date("[Y-m-d H:i:s$t] ").$sql."\n", FILE_APPEND);
}
try
{
$r = $this->link->query($sql);
}
catch (Exception $e)
{
if ($this->queryLogFile)
file_put_contents($this->queryLogFile, date("[Y-m-d H:i:s] ")."Error: $e\n---- Query: $sql\n", FILE_APPEND);
throw $e;
}
return $r;
}
/**
* Starts a transaction, supports nested calls and savepoints.
* @param boolean $savepoint Creates savepoints only this parameter is true.
* @param boolean $savepoint Creates savepoints only if this parameter is true.
*/
function begin($savepoint = false)
{
@ -199,11 +209,11 @@ class DatabasePdoPgsql implements Database
function commit()
{
$r = true;
if (count($this->transactions) == 1)
$r = $this->query("COMMIT");
elseif ($this->transactions)
$r = $this->query("RELEASE SAVEPOINT sp".count($this->transactions));
$savepoint = array_pop($this->transactions);
if (count($this->transactions) == 0)
$r = $this->query("COMMIT");
elseif ($savepoint)
$r = $this->query("RELEASE SAVEPOINT sp".count($this->transactions));
return $r;
}
@ -241,12 +251,12 @@ class DatabasePdoPgsql implements Database
*/
function rollback()
{
$r = false;
$r = true;
$savepoint = array_pop($this->transactions);
if (count($this->transactions) == 1)
$r = $this->query("ROLLBACK");
elseif ($this->transactions)
elseif ($savepoint)
$r = $this->query("ROLLBACK TO SAVEPOINT sp".count($this->transactions));
$savepoint = array_pop($this->transactions);
return $r;
}
@ -312,7 +322,7 @@ class DatabasePdoPgsql implements Database
$wh[] = "$k IS NULL";
}
if ($where)
$where = '(' . join(') AND (', $wh) . ')';
$where = '(' . join(") AND (", $wh) . ')';
else
$where = '';
return $where;
@ -350,7 +360,7 @@ class DatabasePdoPgsql implements Database
foreach ($fields as $k => $v)
if (!ctype_digit("$k"))
$fields[$k] = "$v AS ".$this->quoteId($k);
$fields = join(',', $fields);
$fields = join(",\n ", str_replace("\n", "\n ", $fields));
}
$more = NULL;
$tables = $this->tables_builder($tables, $more);
@ -359,14 +369,14 @@ class DatabasePdoPgsql implements Database
$where = $this->where_builder($where);
$this->calcFoundRows = isset($options['CALC_FOUND_ROWS']) || isset($options['SQL_CALC_FOUND_ROWS']);
if ($this->calcFoundRows)
$fields .= ', COUNT(*) OVER () "*"';
$sql = "SELECT $fields FROM $tables";
$fields .= ",\n COUNT(*) OVER () \"*\"";
$sql = "SELECT $fields\nFROM $tables";
if ($where)
$sql .= " WHERE $where";
$sql .= "\nWHERE $where";
if (!empty($options['GROUP BY']) && $options['GROUP BY'] !== '0')
$sql .= " GROUP BY ".$this->order_option($options['GROUP BY']);
$sql .= "\nGROUP BY ".$this->order_option($options['GROUP BY']);
if (!empty($options['ORDER BY']) && $options['ORDER BY'] !== '0')
$sql .= " ORDER BY ".$this->order_option($options['ORDER BY']);
$sql .= "\nORDER BY ".$this->order_option($options['ORDER BY']);
$sql .= $this->limit_option($options);
if (isset($options['FOR UPDATE']))
$sql .= ' FOR UPDATE';
@ -425,6 +435,7 @@ class DatabasePdoPgsql implements Database
* 'alias' => 'table',
* 'alias' => array('INNER', 'table_name', $where_for_on_clause),
* 'alias(ignored)' => array('INNER', $nested_tables, $on_for_join_group),
* 'alias' => array('INNER', $this->values_table($rows), $where_for_on_clause),
* )
* or just a string "table1 INNER JOIN \"table2\" ON ..."
* escaped names ("table2") will be transformed using $this->tableNames
@ -447,7 +458,12 @@ class DatabasePdoPgsql implements Database
$join = 'FULL';
else /* if (!$join || $join == 'inne' || $join == 'join') */
$join = 'INNER';
if (is_array($v[1])) // nested join (left join (A join B on ...) on ...)
if ($v[1] instanceof DatabasePdoPgsql_Values)
{
$v[1]->alias = $k;
$table = ''.$v[1];
}
elseif (is_array($v[1])) // nested join (left join (A join B on ...) on ...)
{
$more = NULL;
$table = $this->tables_builder($v[1], $more);
@ -463,7 +479,7 @@ class DatabasePdoPgsql implements Database
$table .= ' ' . $k;
}
if ($t)
$t .= " $join JOIN $table ON ".($this->where_builder($v[2]) ?: '1=1');
$t .= "\n$join JOIN $table ON ".($this->where_builder($v[2]) ?: '1=1');
else
{
$t = $table;
@ -553,13 +569,17 @@ class DatabasePdoPgsql implements Database
function delete($tables, $where, $options = NULL)
{
list($what, $using) = $this->split_using($tables, $where);
$more = NULL;
$sql = "DELETE FROM $what".($using ? " USING ".$this->tables_builder($using, $more) : '');
if ($more)
$where = array_merge($where, (array)$more);
$where = $this->where_builder($where) ?: '1=1';
$sql = "DELETE FROM $what".($using ? " USING ".$this->tables_builder($using) : '')." WHERE $where";
$sql .= " WHERE $where";
$sql .= $this->limit_option($options);
return $this->query($sql);
}
function values($rows)
protected function values($rows, $forInsert)
{
$key = reset($rows);
if (!is_array($key))
@ -572,15 +592,25 @@ class DatabasePdoPgsql implements Database
{
$rs = array();
foreach ($key as &$k)
$rs[] = $this->quote(isset($r[$k]) ? $r[$k] : NULL);
{
$rs[] = isset($r[$k]) ? $this->quote($r[$k]) : ($forInsert ? 'DEFAULT' : 'NULL');
}
$r = implode(",", $rs);
}
foreach ($key as &$k)
{
if (strpos($k, '"') === false)
$k = $this->quoteId($k);
}
return array($key, $rows);
}
function values_table($rows, $alias = '')
{
list($keys, $values) = $this->values($rows, false);
return new DatabasePdoPgsql_Values($keys, $values, $alias);
}
/**
* Builds an INSERT query.
*
@ -590,6 +620,8 @@ class DatabasePdoPgsql implements Database
* @param array|string $uniqueColumns unique columns for conflict
* (defaults to {$table}_pkey for "insert or update" queries)
* @param array|NULL $updateCols Columns to update in case of a conflict
* may be array('field1', 'field2', ...) or array('field1' => 'expression', ...)
* NEW.any_field will be replaced with EXCLUDED.any_field in these expressions
*/
function insert_builder($table, $rows, $action = NULL, $uniqueColumns = NULL, $updateCols = NULL)
{
@ -597,7 +629,7 @@ class DatabasePdoPgsql implements Database
{
$table = $this->quoteId($this->tableNames[$table]);
}
list($keys, $values) = $this->values($rows);
list($keys, $values) = $this->values($rows, true);
$sql = "INSERT INTO $table (".implode(',', $keys).") VALUES (".implode('),(', $values).")";
if ($action)
{
@ -615,8 +647,13 @@ class DatabasePdoPgsql implements Database
$key = reset($rows);
$key = is_array($key) ? array_keys($key) : array_keys($rows);
}
foreach ($key as &$k)
$k = "$k = EXCLUDED.$k";
foreach ($key as $i => &$k)
{
if (is_intval($i))
$k = "$k = EXCLUDED.$k";
else
$k = "$i = ".str_replace('NEW.', 'EXCLUDED.', $k);
}
$sql .= " DO UPDATE SET ".implode(", ", $key);
}
else
@ -658,49 +695,50 @@ class DatabasePdoPgsql implements Database
return $this->link->lastInsertId();
}
/**
* Разбивает набор таблиц на основную обновляемую + набор дополнительных
*
* Идея в том, чтобы обрабатывать хотя бы 2 простые ситуации:
* UPDATE table1 INNER JOIN table2 ...
* UPDATE table1 LEFT JOIN table2 ...
*/
protected function split_using($tables, &$where)
{
$first = $has_inner = NULL;
$tables = (array)$tables;
$first = NULL;
$isNextInner = true;
$i = 0;
foreach ($tables as $k => $t)
{
if (!is_array($t))
if ($i == 0)
{
if ($first === NULL)
if (is_array($t) && ($t[1] instanceof DatabasePdoPgsql_Values || is_array($t[1])))
{
$first = $k;
}
elseif (!$has_inner)
{
$tables = array($k => array('INNER', $t)) + $tables;
$has_inner = true;
break;
throw new DatabaseException("Can only update/delete from real tables, not sub-select, sub-join or VALUES");
}
$first = $k;
}
elseif (in_array(strtolower($t[0]), array('inner', 'cross', 'join')))
elseif ($i == 1)
{
$tables = array($k => $t) + $tables;
$has_inner = true;
if ($first !== NULL)
{
break;
}
$isNextInner = !is_array($t) || strtolower($t[0]) == 'inner';
}
else
break;
$i++;
}
if (count($tables) > 1 && !$has_inner)
$more = NULL;
if ($isNextInner)
{
if ($first === NULL)
{
throw new DatabaseException("No update/delete subject found");
}
$what = $this->tables_builder([ "_$first" => $tables[$first] ]);
$where[] = "_$first.ctid=".(ctype_digit("$first") ? $tables[$first] : $first).".ctid";
$what = $this->tables_builder([ $first => $tables[$first] ], $more);
unset($tables[$first]);
}
else
{
$what = $this->tables_builder([ $first => $tables[$first] ]);
unset($tables[$first]);
$what = $this->tables_builder([ "_$first" => $tables[$first] ], $more);
$where[] = "_$first.ctid=".(ctype_digit("$first") ? $tables[$first] : $first).".ctid";
}
if ($more)
$where = array_merge($where, (array)$more);
return array($what, $tables);
}
@ -725,9 +763,13 @@ class DatabasePdoPgsql implements Database
$sql[] = $v;
}
list($what, $using) = $this->split_using($table, $where);
$where = $this->where_builder($where) ?: '1=1';
$more = NULL;
$sql = 'UPDATE ' . $what . ' SET ' . implode(', ', $sql) .
($using ? ' FROM '.$this->tables_builder($using) : '') . ' WHERE ' . $where;
($using ? ' FROM '.$this->tables_builder($using, $more) : '');
if ($more)
$where = array_merge($where, $more);
$where = $this->where_builder($where) ?: '1=1';
$sql .= ' WHERE ' . $where;
$r = $this->query($sql);
if ($r)
return $r->rowCount();
@ -780,3 +822,18 @@ class DatabasePdoPgsql implements Database
return $n;
}
}
class DatabasePdoPgsql_Values
{
function __construct($keys, $values, $alias)
{
$this->keys = $keys;
$this->values = $values;
$this->alias = $alias;
}
function __toString()
{
return "(VALUES (".implode("),(", $this->values).")) AS ".$this->alias." (".implode(',', $this->keys).")";
}
}