INSERT OR REPLACE implementation (#6771)

For now with support in postgres and mysql databases.
For now used in rcube_cache, rcube_imap_cache and enigma plugin
pull/7195/head
Aleksander Machniak 5 years ago committed by GitHub
parent e4281ae6d4
commit 1613f3ab4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -676,17 +676,8 @@ class enigma_driver_gnupg extends enigma_driver
continue;
}
if (empty($existing)) {
$result = $db->query(
"INSERT INTO $table (`user_id`, `context`, `filename`, `mtime`, `data`)"
. " VALUES(?, 'enigma', ?, ?, ?)",
$this->rc->user->ID, $filename, $mtime, $data);
}
else {
$result = $db->query(
"UPDATE $table SET `mtime` = ?, `data` = ? WHERE `file_id` = ?",
$mtime, $data, $existing['file_id']);
}
$unique = array('user_id' => $this->rc->user->ID, 'context' => 'enigma', 'filename' => $filename);
$result = $db->insert_or_update($table, $unique, array('mtime', 'data'), array($mtime, $data));
if ($db->is_error($result)) {
rcube::raise_error(array(

@ -165,39 +165,18 @@ class rcube_cache_db extends rcube_cache
return !$this->db->is_error($result);
}
$key_exists = array_key_exists($key, $this->cache_sums);
$expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if (!$key_exists) {
// Try INSERT temporarily ignoring "duplicate key" errors
$this->db->set_option('ignore_key_errors', true);
if ($this->userid) {
$result = $this->db->query(
"INSERT INTO {$this->table} (`expires`, `user_id`, `cache_key`, `data`)"
. " VALUES ($expires, ?, ?, ?)",
$this->userid, $db_key, $data);
}
else {
$result = $this->db->query(
"INSERT INTO {$this->table} (`expires`, `cache_key`, `data`)"
. " VALUES ($expires, ?, ?)",
$db_key, $data);
}
$expires = $this->db->param($this->ttl ? $this->db->now($this->ttl) : 'NULL', rcube_db::TYPE_SQL);
$pkey = array('cache_key' => $db_key);
$this->db->set_option('ignore_key_errors', false);
if ($this->userid) {
$pkey['user_id'] = $this->userid;
}
// otherwise try UPDATE
if (!isset($result) || !($count = $this->db->affected_rows($result))) {
$result = $this->db->query(
"UPDATE {$this->table} SET `expires` = $expires, `data` = ? WHERE "
. ($this->userid ? "`user_id` = {$this->userid} AND " : "")
. "`cache_key` = ?",
$data, $db_key);
$result = $this->db->insert_or_update(
$this->table, $pkey, array('expires', 'data'), array($expires, $data)
);
$count = $this->db->affected_rows($result);
}
$count = $this->db->affected_rows($result);
return $count > 0;
}

@ -231,6 +231,33 @@ class rcube_db_mysql extends rcube_db
return $this->variables[$varname];
}
/**
* INSERT ... ON DUPLICATE KEY UPDATE (or equivalent).
* When not supported by the engine we do UPDATE and INSERT.
*
* @param string $table Table name
* @param array $keys Hash array (column => value) of the unique constraint
* @param array $columns List of columns to update
* @param array $values List of values to update (number of elements
* should be the same as in $columns)
*
* @return PDOStatement|bool Query handle or False on error
* @todo Multi-insert support
*/
public function insert_or_update($table, $keys, $columns, $values)
{
$table = $this->table_name($table, true);
$columns = array_map(function($i) { return "`$i`"; }, $columns);
$cols = implode(', ', array_map(function($i) { return "`$i`"; }, array_keys($keys)));
$cols .= ', ' . implode(', ', $columns);
$vals = implode(', ', array_map(function($i) { return $this->quote($i); }, $keys));
$vals .= ', ' . rtrim(str_repeat('?, ', count($columns)), ', ');
$update = implode(', ', array_map(function($i) { return "$i = VALUES($i)"; }, $columns));
return $this->query("INSERT INTO $table ($cols) VALUES ($vals)"
. " ON DUPLICATE KEY UPDATE $update", $values);
}
/**
* Handle DB errors, re-issue the query on deadlock errors from InnoDB row-level locking
*

@ -0,0 +1,58 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Database wrapper class for query parameters |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Database query parameter
*
* @package Framework
* @subpackage Database
*/
class rcube_db_param
{
protected $db;
protected $type;
protected $value;
/**
* Object constructor
*
* @param rcube_db $db Database driver
* @param mixed $value Parameter value
* @param string $type Parameter type (One of rcube_db::TYPE_* constants)
*/
public function __construct($db, $value, $type = null)
{
$this->db = $db;
$this->value = $value;
$this->type = $type;
}
/**
* Returns the value as string for inlining into SQL query
*/
public function __toString()
{
if ($this->type === rcube_db::TYPE_SQL) {
return (string) $this->value;
}
return (string) $this->db->quote($this->value, $this->type);
}
}

@ -182,6 +182,38 @@ class rcube_db_pgsql extends rcube_db
return isset($this->variables[$varname]) ? $this->variables[$varname] : $default;
}
/**
* INSERT ... ON CONFLICT DO UPDATE.
* When not supported by the engine we do UPDATE and INSERT.
*
* @param string $table Table name
* @param array $keys Hash array (column => value) of the unique constraint
* @param array $columns List of columns to update
* @param array $values List of values to update (number of elements
* should be the same as in $columns)
*
* @return PDOStatement|bool Query handle or False on error
* @todo Multi-insert support
*/
public function insert_or_update($table, $keys, $columns, $values)
{
// Check if version >= 9.5, otherwise use fallback
if ($this->get_variable('server_version_num') < 90500) {
return parent::insert_or_update($table, $keys, $columns, $values);
}
$table = $this->table_name($table, true);
$columns = array_map(array($this, 'quote_identifier'), $columns);
$target = implode(', ', array_map(array($this, 'quote_identifier'), array_keys($keys)));
$cols = $target . ', ' . implode(', ', $columns);
$vals = implode(', ', array_map(function($i) { return $this->quote($i); }, $keys));
$vals .= ', ' . rtrim(str_repeat('?, ', count($columns)), ', ');
$update = implode(', ', array_map(function($i) { return "$i = EXCLUDED.$i"; }, $columns));
return $this->query("INSERT INTO $table ($cols) VALUES ($vals)"
. " ON CONFLICT ($target) DO UPDATE SET $update", $values);
}
/**
* Returns list of tables in a database
*

@ -55,6 +55,12 @@ class rcube_db
const DEBUG_LINE_LENGTH = 4096;
const DEFAULT_QUOTE = '`';
const TYPE_SQL = 'sql';
const TYPE_INT = 'integer';
const TYPE_BOOL = 'bool';
const TYPE_STRING = 'string';
/**
* Factory, returns driver-specific instance of the class
*
@ -549,6 +555,46 @@ class rcube_db
return false;
}
/**
* INSERT ... ON DUPLICATE KEY UPDATE (or equivalent).
* When not supported by the engine we do UPDATE and INSERT.
*
* @param string $table Table name
* @param array $keys Hash array (column => value) of the unique constraint
* @param array $columns List of columns to update
* @param array $values List of values to update (number of elements
* should be the same as in $columns)
*
* @return PDOStatement|bool Query handle or False on error
* @todo Multi-insert support
*/
public function insert_or_update($table, $keys, $columns, $values)
{
$table = $this->table_name($table, true);
$columns = array_map(function($i) { return "`$i`"; }, $columns);
$sets = array_map(function($i) { return "$i = ?"; }, $columns);
array_walk($where, function(&$val, $key) {
$val = $this->quote_identifier($key) . " = " . $this->quote($val);
});
// First try UPDATE
$result = $this->query("UPDATE $table SET " . implode(", ", $sets)
. " WHERE " . implode(" AND ", $where), $values);
// if UPDATE fails use INSERT
if ($result && !$this->affected_rows($result)) {
$cols = implode(', ', array_map(function($i) { return "`$i`"; }, array_keys($keys)));
$cols .= ', ' . implode(', ', $columns);
$vals = implode(', ', array_map(function($i) { return $this->quote($i); }, $keys));
$vals .= ', ' . rtrim(str_repeat('?, ', count($columns)), ', ');
$result = $this->query("INSERT INTO $table ($cols) VALUES ($vals)", $values);
}
return $result;
}
/**
* Get number of affected rows for the last query
*
@ -813,6 +859,10 @@ class rcube_db
*/
public function quote($input, $type = null)
{
if ($input instanceof rcube_db_param) {
return (string) $input;
}
// handle int directly for better performance
if ($type == 'integer' || $type == 'int') {
return intval($input);
@ -917,6 +967,17 @@ class rcube_db
return implode('.', $name);
}
/**
* Create query parameter object
*
* @param mixed $value Parameter value
* @param string $type Parameter type (one of rcube_db::TYPE_* constants)
*/
public function param($value, $type = null)
{
return new rcube_db_param($this, $value, $type);
}
/**
* Return SQL function for current time and date
*

@ -474,47 +474,16 @@ class rcube_imap_cache
}
unset($msg->flags);
$msg = $this->db->encode($msg, true);
// update cache record (even if it exists, the update
// here will work as select, assume row exist if affected_rows=0)
if (!$force) {
$res = $this->db->query(
"UPDATE {$this->messages_table}"
." SET `flags` = ?, `data` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
." WHERE `user_id` = ?"
." AND `mailbox` = ?"
." AND `uid` = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
// insert new record
$res = $this->db->query(
"INSERT INTO {$this->messages_table}"
." (`user_id`, `mailbox`, `uid`, `flags`, `expires`, `data`)"
." VALUES (?, ?, ?, ?, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?)",
$this->userid, $mailbox, (int) $message->uid, $flags, $msg);
$msg = $this->db->encode($msg, true);
$expires = $this->db->param($this->ttl ? $this->db->now($this->ttl) : 'NULL', rcube_db::TYPE_SQL);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if ($force && !$res && !$this->db->is_error($res)) {
$this->db->query(
"UPDATE {$this->messages_table}"
." SET `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
.", `flags` = ?, `data` = ?"
." WHERE `user_id` = ?"
." AND `mailbox` = ?"
." AND `uid` = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
}
$this->db->set_option('ignore_key_errors', false);
$this->db->insert_or_update(
$this->messages_table,
array('user_id' => $this->userid, 'mailbox' => $mailbox, 'uid' => (int) $message->uid),
array('flags', 'expires', 'data'),
array($flags, $expires, $msg)
);
}
/**
@ -801,41 +770,14 @@ class rcube_imap_cache
);
$data = implode('@', $data);
$expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if ($exists) {
$res = $this->db->query(
"UPDATE {$this->index_table}"
." SET `data` = ?, `valid` = 1, `expires` = $expires"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
if ($this->db->affected_rows($res)) {
return;
}
}
$expires = $this->db->param($this->ttl ? $this->db->now($this->ttl) : 'NULL', rcube_db::TYPE_SQL);
$this->db->set_option('ignore_key_errors', true);
$res = $this->db->query(
"INSERT INTO {$this->index_table}"
." (`user_id`, `mailbox`, `valid`, `expires`, `data`)"
." VALUES (?, ?, 1, $expires, ?)",
$this->userid, $mailbox, $data);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if (!$exists && !$res && !$this->db->is_error($res)) {
$res = $this->db->query(
"UPDATE {$this->index_table}"
." SET `data` = ?, `valid` = 1, `expires` = $expires"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
}
$this->db->set_option('ignore_key_errors', false);
$this->db->insert_or_update(
$this->index_table,
array('user_id' => $this->userid, 'mailbox' => $mailbox),
array('valid', 'expires', 'data'),
array(1, $expires, $data)
);
}
/**
@ -855,41 +797,14 @@ class rcube_imap_cache
);
$data = implode('@', $data);
$expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if ($exists) {
$res = $this->db->query(
"UPDATE {$this->thread_table}"
." SET `data` = ?, `expires` = $expires"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
$expires = $this->db->param($this->ttl ? $this->db->now($this->ttl) : 'NULL', rcube_db::TYPE_SQL);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
$res = $this->db->query(
"INSERT INTO {$this->thread_table}"
." (`user_id`, `mailbox`, `expires`, `data`)"
." VALUES (?, ?, $expires, ?)",
$this->userid, $mailbox, $data);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if (!$exists && !$res && !$this->db->is_error($res)) {
$this->db->query(
"UPDATE {$this->thread_table}"
." SET `expires` = $expires, `data` = ?"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
}
$this->db->set_option('ignore_key_errors', false);
$this->db->insert_or_update(
$this->thread_table,
array('user_id' => $this->userid, 'mailbox' => $mailbox),
array('expires', 'data'),
array($expires, $data)
);
}
/**

Loading…
Cancel
Save