[DATABASE] Update "modified" in Managed_DataObject instead of a DBMS trigger

Instead of relying on the MariaDB's ON UPDATE CURRENT_TIMESTAMP trigger update
"modified" attributes in Managed_DataObject. Every raw query that needs
adjusting is adjusted, as they won't update "modified" automatically anymore.

The main goal behind this change is to fix "modified" updates on PostgreSQL.
This commit is contained in:
Alexei Sorokin 2020-07-27 19:10:33 +03:00 committed by Diogo Peralta Cordeiro
parent 346aec9b2a
commit b1b1d2af93
17 changed files with 224 additions and 140 deletions

View File

@ -760,9 +760,12 @@ class File extends Managed_DataObject
if ($file instanceof File) {
throw new ServerException('URL already exists in DB');
}
$sql = 'UPDATE %1$s SET urlhash = %2$s, url = %3$s WHERE urlhash = %4$s;';
$result = $this->query(sprintf(
$sql,
<<<'END'
UPDATE %1$s
SET urlhash = %2$s, url = %3$s, modified = CURRENT_TIMESTAMP
WHERE urlhash = %4$s;
END,
$this->tableName(),
$this->_quote((string)self::hashurl($url)),
$this->_quote((string)$url),
@ -973,7 +976,7 @@ class File extends Managed_DataObject
throw new ServerException('Unknown DB type selected.');
}
$tablefix->query(sprintf(
'UPDATE %1$s SET urlhash = %2$s;',
'UPDATE %1$s SET urlhash = %2$s, modified = CURRENT_TIMESTAMP;',
$tablefix->escapedTableName(),
$url_sha256
));

View File

@ -468,7 +468,7 @@ class File_redirection extends Managed_DataObject
throw new ServerException('Unknown DB type selected.');
}
$tablefix->query(sprintf(
'UPDATE %1$s SET urlhash = %2$s;',
'UPDATE %1$s SET urlhash = %2$s, modified = CURRENT_TIMESTAMP;',
$tablefix->escapedTableName(),
$url_sha256
));

View File

@ -74,15 +74,20 @@ class Local_group extends Managed_DataObject
public function setNickname($nickname)
{
$this->decache();
$modified = common_sql_now();
$result = $this->query(sprintf(
'UPDATE local_group SET nickname = %1$s WHERE group_id = %2$d;',
<<<'END'
UPDATE local_group SET nickname = %1$s, modified = %2$s
WHERE group_id = %3$d;
END,
$this->_quote($nickname),
$this->_quote($modified),
$this->group_id
));
if ($result) {
$this->nickname = $nickname;
$this->fixupTimestamps();
$this->modified = $modified;
$this->encache();
} else {
common_log_db_error($local, 'UPDATE', __FILE__);

View File

@ -522,6 +522,25 @@ abstract class Managed_DataObject extends Memcached_DataObject
return $aliases;
}
/**
* Set the attribute defined as "timestamp" to CURRENT_TIMESTAMP.
* This is hooked in update() and updateWithKeys() to update "modified".
*
* @access private
* @return void
*/
private function updateAutoTimestamps(): void
{
$table = static::schemaDef();
foreach ($table['fields'] as $name => $col) {
if ($col['type'] === 'timestamp'
&& !array_key_exists('default', $col)
&& !isset($this->$name)) {
$this->$name = common_sql_now();
}
}
}
/**
* update() won't write key columns, so we have to do it ourselves.
* This also automatically calls "update" _before_ it sets the keys.
@ -548,6 +567,10 @@ abstract class Managed_DataObject extends Memcached_DataObject
// do it in a transaction
$this->query('START TRANSACTION');
// ON UPDATE CURRENT_TIMESTAMP behaviour
// @fixme Should the value be reverted back if transaction failed?
$this->updateAutoTimestamps();
$parts = [];
foreach ($this->keys() as $k) {
$v = $this->table()[$k];
@ -664,12 +687,25 @@ abstract class Managed_DataObject extends Memcached_DataObject
public function insert()
{
$this->onInsert();
return parent::insert();
$result = parent::insert();
// Make this object aware of the changed "modified" attribute.
// Sets it approximately to the same value as DEFAULT CURRENT_TIMESTAMP
// just did (@fixme).
if ($result) {
$this->updateAutoTimestamps();
}
return $result;
}
public function update($dataObject = false)
{
$this->onUpdate($dataObject);
// ON UPDATE CURRENT_TIMESTAMP behaviour
// @fixme Should the value be reverted back if transaction failed?
$this->updateAutoTimestamps();
return parent::update($dataObject);
}
}

View File

@ -443,7 +443,6 @@ class Memcached_DataObject extends Safe_DataObject
{
$result = parent::insert();
if ($result) {
$this->fixupTimestamps();
$this->encache(); // in case of cached negative lookups
}
return $result;
@ -456,7 +455,6 @@ class Memcached_DataObject extends Safe_DataObject
}
$result = parent::update($dataObject);
if ($result !== false) {
$this->fixupTimestamps();
$this->encache();
}
return $result;
@ -931,22 +929,6 @@ class Memcached_DataObject extends Safe_DataObject
return $c->delete($cacheKey);
}
public function fixupTimestamps()
{
// Fake up timestamp columns
$columns = $this->table();
foreach ($columns as $name => $type) {
if ($type & DB_DATAOBJECT_MYSQLTIMESTAMP) {
$this->$name = common_sql_now();
}
}
}
public function debugDump()
{
common_debug("debugDump: " . common_log_objstring($this));
}
public function raiseError($message, $type = null, $behavior = null)
{
$id = get_class($this);

View File

@ -3243,7 +3243,11 @@ class Notice extends Managed_DataObject
unset($notice);
$notice = new Notice();
$notice->query(sprintf(
'UPDATE %1$s SET %2$s = NULL WHERE id IN (%3$s)',
<<<'END'
UPDATE %1$s
SET %2$s = NULL, modified = CURRENT_TIMESTAMP
WHERE id IN (%3$s)
END,
$notice->escapedTableName(),
$field,
implode(',', $ids)

View File

@ -86,6 +86,7 @@ class Oauth_application_user extends Managed_DataObject
return true;
}
$toupdate = implode(', ', $parts);
$toupdate .= ', modified = CURRENT_TIMESTAMP';
$table = $this->tableName();
$tableName = $this->escapedTableName();

View File

@ -292,14 +292,16 @@ class Profile_tag extends Managed_DataObject
public static function moveTag($orig, $new)
{
$tags = new Profile_tag();
$qry = "UPDATE profile_tag SET tag = '%s', tagger = '%s' " .
"WHERE tag = '%s' AND tagger = '%s'";
$result = $tags->query(sprintf(
$qry,
$tags->escape($new->tag),
$tags->escape($new->tagger),
$tags->escape($orig->tag),
$tags->escape($orig->tagger)
<<<'END'
UPDATE profile_tag
SET tag = %1$s, tagger = %2$s, modified = CURRENT_TIMESTAMP
WHERE tag = %3$s AND tagger = %4$s
END,
$tags->_quote($new->tag),
$tags->_quote($new->tagger),
$tags->_quote($orig->tag),
$tags->_quote($orig->tagger)
));
if ($result === false) {

View File

@ -104,11 +104,22 @@ class Queue_item extends Managed_DataObject
*/
public function releaseClaim()
{
// DB_DataObject doesn't let us save nulls right now
$sql = sprintf("UPDATE queue_item SET claimed=NULL WHERE id=%d", $this->getID());
$this->query($sql);
$modified = common_sql_now();
// @fixme Consider $this->sqlValue('NULL')
$ret = $this->query(sprintf(
<<<'END'
UPDATE queue_item
SET claimed = NULL, modified = %1$s
WHERE id = %2$d
END,
$this->_quote($modified),
$this->getID()
));
if ($ret) {
$this->claimed = null;
$this->modified = $modified;
$this->encache();
}
}
}

View File

@ -145,12 +145,6 @@ class MysqlSchema extends Schema
if (preg_match('/(^|\s)auto_increment(\s|$)/i', $extra)) {
$field['auto_increment'] = true;
}
if (preg_match(
'/(^|\s)on update CURRENT_TIMESTAMP(\(\))?(\s|$)/i',
$extra
)) {
$field['auto_update_timestamp'] = true;
}
}
$table_props = $this->getTableProperties($table, ['TABLE_COLLATION']);
@ -519,10 +513,6 @@ class MysqlSchema extends Schema
if (!empty($cd['auto_increment'])) {
$line[] = 'AUTO_INCREMENT';
}
// This'll have been added from our transform of "timestamp" type
if (!empty($cd['auto_update_timestamp'])) {
$line[] = 'ON UPDATE CURRENT_TIMESTAMP';
}
if (!empty($cd['description'])) {
$line[] = 'COMMENT';

View File

@ -1043,7 +1043,6 @@ class Schema
$col['type'] = 'datetime';
if (!array_key_exists('default', $col)) {
$col['default'] = 'CURRENT_TIMESTAMP';
// FIXME: PostgreSQL support.
$col['auto_update_timestamp'] = true;
}
// no break

View File

@ -116,10 +116,15 @@ while ($fn->fetch()) {
echo " (unchanged, but embedding lookup failed)\n";
}
} elseif (!$dry) {
$sql = "UPDATE file " .
"SET mimetype=null, title=null,size=null,protected=null " .
"WHERE id={$f->id}";
$f->query($sql);
$f->query(sprintf(
<<<'END'
UPDATE file
SET mimetype = NULL, title = NULL, size = NULL,
protected = NULL, modified = CURRENT_TIMESTAMP
WHERE id = %d
END,
$f->getID()
));
$f->decache();
if ($data instanceof File_embed) {
$fetch = true;
@ -144,12 +149,18 @@ while ($fn->fetch()) {
echo "Found broken file with ";
}
echo "ID: {$f->id}, URL {$f->url}\n";
echo "ID: {$f->getID()}, URL {$f->url}\n";
if (!$dry) {
$fetch = true;
$sql = "UPDATE file SET title=null, size=null, protected=null " .
"WHERE id={$f->id}";
$f->query($sql);
$f->query(sprintf(
<<<'END'
UPDATE file
SET title = NULL, size = NULL,
protected = NULL, modified = CURRENT_TIMESTAMP
WHERE id = %d,
END,
$f->getID()
));
$f->decache();
if ($data instanceof File_embed) {
@ -159,7 +170,7 @@ while ($fn->fetch()) {
if ($thumb instanceof File_thumbnail) {
// Delete all thumbnails, not just this one
$f->query("DELETE from file_thumbnail WHERE file_id = {$f->id}");
$f->query("DELETE FROM file_thumbnail WHERE file_id = {$f->getID()}");
$thumb->decache();
}
}

View File

@ -109,9 +109,10 @@ if ($feedurl != $oprofile->feeduri || $salmonuri != $oprofile->salmonuri) {
$ok = $oprofile->query(
<<<END
UPDATE ostatus_profile
SET feeduri = '{$oprofile->escape($feedurl)}',
salmonuri = '{$oprofile->escape($salmonuri)}'
WHERE uri = '{$oprofile->escape($uri)}'
SET feeduri = {$oprofile->_quote($feedurl)},
salmonuri = {$oprofile->_quote($salmonuri)},
modified = CURRENT_TIMESTAMP
WHERE uri = {$oprofile->_quote($uri)}
END
);

View File

@ -1,45 +1,39 @@
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
/**
* StatusNet, the distributed open-source microblogging tool
*
* Class to ping an rssCloud endpoint when a feed has been updated
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Plugin
* @package StatusNet
* @package GNUsocial
* @author Zach Copley <zach@status.net>
* @copyright 2009 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
if (!defined('STATUSNET')) {
exit(1);
}
defined('STATUSNET') || die();
/**
* Class for notifying cloud-enabled RSS aggregators that StatusNet
* feeds have been updated.
*
* @category Plugin
* @package StatusNet
* @package GNUsocial
* @author Zach Copley <zach@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class RSSCloudNotifier
{
@ -54,7 +48,7 @@ class RSSCloudNotifier
*
* @return boolean success
*/
function challenge($endpoint, $feed)
public function challenge($endpoint, $feed)
{
$code = common_confirmation_code(128);
$params = array('url' => $feed, 'challenge' => $code);
@ -64,9 +58,11 @@ class RSSCloudNotifier
$client = new HTTPClient();
$response = $client->get($url);
} catch (Exception $e) {
common_log(LOG_INFO,
'RSSCloud plugin - failure testing notify handler ' .
$endpoint . ' - ' . $e->getMessage());
common_log(
LOG_INFO,
'RSSCloud plugin - failure testing notify handler '
. $endpoint . ' - ' . $e->getMessage()
);
return false;
}
@ -110,7 +106,7 @@ class RSSCloudNotifier
*
* @return boolean success
*/
function postUpdate($endpoint, $feed)
public function postUpdate($endpoint, $feed)
{
$headers = array();
$postdata = array('url' => $feed);
@ -147,7 +143,7 @@ class RSSCloudNotifier
*
* @return boolean success
*/
function notify($profile)
public function notify($profile)
{
$feed = common_path('api/statuses/user_timeline/') .
$profile->id . '.rss';
@ -180,18 +176,16 @@ class RSSCloudNotifier
*
* @return boolean success
*/
function handleFailure($cloudSub)
public function handleFailure($cloudSub)
{
$failCnt = $cloudSub->failures + 1;
if ($failCnt == self::MAX_FAILURES) {
common_log(LOG_INFO,
'Deleting RSSCloud subcription ' .
'(max failure count reached), profile: ' .
$cloudSub->subscribed .
' handler: ' .
$cloudSub->url);
common_log(
LOG_INFO,
'Deleting RSSCloud subcription (max failure count reached), '
. "profile: {$cloudSub->subscribed} handler: {$cloudSub->url}"
);
// XXX: WTF! ->delete() doesn't work. Clearly, there are some issues with
// the DB_DataObject, or my understanding of it. Have to drop into SQL.
@ -216,18 +210,23 @@ class RSSCloudNotifier
// XXX: ->update() not working either, gar!
$qry = 'UPDATE rsscloud_subscription' .
' SET failures = ' . $failCnt .
' WHERE subscribed = ' . $cloudSub->subscribed .
' AND url = \'' . $cloudSub->url . '\'';
$result = $cloudSub->query($qry);
$result = $cloudSub->query(sprintf(
<<<'END'
UPDATE rsscloud_subscription
SET failures = %1$d, modified = CURRENT_TIMESTAMP
WHERE subscribed = %2$d AND url = %3$s
END,
$failCnt,
$cloudSub->subscribed,
$cloudSub->_quote($cloudSub->url)
));
if (!$result) {
common_log_db_error($cloudsub, 'UPDATE', __FILE__);
common_log(LOG_ERR,
'Could not update failure ' .
'count on RSSCloud subscription');
common_log(
LOG_ERR,
'Could not update failure count on RSSCloud subscription'
);
}
}
}

View File

@ -89,10 +89,14 @@ class ClearflagAction extends ProfileFormAction
{
$ufp = new User_flag_profile();
$result = $ufp->query('UPDATE user_flag_profile ' .
'SET cleared = CURRENT_TIMESTAMP ' .
'WHERE cleared IS NULL ' .
'AND profile_id = ' . $this->profile->id);
$result = $ufp->query(sprintf(
<<<'END'
UPDATE user_flag_profile
SET cleared = CURRENT_TIMESTAMP, modified = cleared
WHERE cleared IS NULL AND profile_id = %d
END,
$this->profile->getID()
));
if ($result === false) {
// TRANS: Server exception given when flags could not be cleared.

View File

@ -104,10 +104,15 @@ if ($notice->N) {
// but we're going to have to decache them individually anyway and
// it doesn't hurt to make sure we don't hold up replication with
// what might be a very slow single UPDATE.
$query = sprintf('UPDATE notice ' .
'SET lat=NULL,lon=NULL,location_ns=NULL,location_id=NULL ' .
'WHERE id=%d', $notice->id);
$ok = $update->query($query);
$ok = $update->query(sprintf(
<<<'END'
UPDATE notice
SET lat = NULL, lon = NULL, location_ns = NULL, location_id = NULL
modified = CURRENT_TIMESTAMP
WHERE id = %d
END,
$notice->getID()
));
if ($ok) {
// And now we decache him manually, as query() doesn't know what we're doing...
$orig->decache();

View File

@ -131,9 +131,16 @@ function fixupUserBadNulls(): void
if ($user->find()) {
while ($user->fetch()) {
$sql = "UPDATE {$user->escapedTableName()} SET {$col} = NULL "
. "WHERE id = {$user->id}";
$user->query($sql);
$user->query(sprintf(
<<<'END'
UPDATE %1$s
SET %2$s = NULL, modified = CURRENT_TIMESTAMP
WHERE id = %3$d
END,
$user->escapedTableName(),
$col,
$user->getID()
));
}
}
}
@ -269,8 +276,12 @@ function fixupConversationURIs()
while ($conv->fetch()) {
$uri = common_local_url('conversation', ['id' => $conv->id]);
$sql = sprintf(
'UPDATE conversation SET uri = \'%1$s\' WHERE id = %2$d;',
$conv->escape($uri),
<<<'END'
UPDATE conversation
SET uri = %1$s, modified = CURRENT_TIMESTAMP
WHERE id = %2$d;
END,
$conv->_quote($uri),
$conv->id
);
$conv->query($sql);
@ -310,7 +321,15 @@ function initGroupProfileId()
$profile->query('ROLLBACK');
throw new Exception('Profile insertion failed, profileurl: '.$profile->profileurl);
}
$group->query("UPDATE user_group SET profile_id={$id} WHERE id={$group->id}");
$group->query(sprintf(
<<<'END'
UPDATE user_group
SET profile_id = %1$d, modified = CURRENT_TIMESTAMP
WHERE id = %2$d
END,
$id,
$group->getID()
));
$profile->query('COMMIT');
$profile->free();
@ -400,18 +419,24 @@ function initSubscriptionURI()
if ($sub->find()) {
while ($sub->fetch()) {
try {
$uri = Subscription::newUri(
$sub->getSubscriber(),
$sub->getSubscribed(),
$sub->created
);
$sub->decache();
$sub->query(sprintf(
'UPDATE subscription '.
"SET uri = '%s' " .
'WHERE subscriber = %d '.
'AND subscribed = %d',
$sub->escape(Subscription::newUri($sub->getSubscriber(), $sub->getSubscribed(), $sub->created)),
<<<'END'
UPDATE subscription
SET uri = %1$s, modified = CURRENT_TIMESTAMP
WHERE subscriber = %2$d AND subscribed = %3$d
END,
$sub->_quote($uri),
$sub->subscriber,
$sub->subscribed
));
} catch (Exception $e) {
common_log(LOG_ERR, "Error updated subscription URI: " . $e->getMessage());
common_log(LOG_ERR, 'Error updating subscription URI: ' . $e->getMessage());
}
}
}
@ -429,13 +454,19 @@ function initGroupMemberURI()
if ($mem->find()) {
while ($mem->fetch()) {
try {
$uri = Group_member::newUri(
Profile::getByID($mem->profile_id),
User_group::getByID($mem->group_id),
$mem->created
);
$mem->decache();
$mem->query(sprintf(
'UPDATE group_member '.
"SET uri = '%s' " .
'WHERE profile_id = %d ' .
'AND group_id = %d',
Group_member::newUri(Profile::getByID($mem->profile_id), User_group::getByID($mem->group_id), $mem->created),
<<<'END'
UPDATE group_member
SET uri = %s, modified = CURRENT_TIMESTAMP
WHERE profile_id = %d AND group_id = %d
END,
$mem->_quote($uri),
$mem->profile_id,
$mem->group_id
));
@ -643,7 +674,7 @@ function fixupFileThumbnailUrlhash()
$thumb = new File_thumbnail();
$thumb->query(sprintf(
'UPDATE %1$s ' .
'SET urlhash = %2$s ' .
'SET urlhash = %2$s, modified = CURRENT_TIMESTAMP ' .
'WHERE url IS NOT NULL ' . // find all entries with a url value
"AND url <> '' " . // precaution against non-null empty strings
'AND urlhash IS NULL', // but don't touch those we've already calculated