Commit 4f083581 authored by Cecilia Vela Gurovic's avatar Cecilia Vela Gurovic Committed by Gerrit Code Review

Merge changes from topic 'Triggers'

* changes:
  Bug 1785985: Remove quota triggers
  Bug 1785985: Remove notification triggers
  Bug 1785985: Remove Elasticsearch triggers
parents 55d0e8c9 8e7a3135
......@@ -126,39 +126,13 @@ class PluginArtefactFile extends PluginArtefact {
public static function set_quota_triggers() {
set_config_plugin('artefact', 'file', 'quotanotifylimit', 80);
set_config_plugin('artefact', 'file', 'quotanotifyadmin', false);
// Create triggers to reset the quota notification flag
if (is_postgres()) {
$sql = "DROP FUNCTION IF EXISTS {unmark_quota_exeed_upd_set}() CASCADE;";
execute_sql($sql);
db_create_trigger(
'unmark_quota_exceed_upd_usr_set',
'AFTER', 'UPDATE', 'usr', "
UPDATE {usr_account_preference}
SET value = 0 FROM {artefact_config}
WHERE {usr_account_preference}.field = 'quota_exceeded_notified'
AND {usr_account_preference}.usr = NEW.id
AND {artefact_config}.plugin = 'file'
AND {artefact_config}.field = 'quotanotifylimit'
AND CAST(NEW.quotaused AS float)/CAST(NEW.quota AS float) < CAST({artefact_config}.value AS float)/100;"
);
}
else {
$sql = "DROP TRIGGER IF EXISTS {unmark_quota_exceed_upd_set}";
execute_sql($sql);
db_create_trigger(
'unmark_quota_exceed_upd_usr_set',
'AFTER', 'UPDATE', 'usr', "
UPDATE {usr_account_preference}, {artefact_config}
SET {usr_account_preference}.value = 0
WHERE {usr_account_preference}.field = 'quota_exceeded_notified'
AND {usr_account_preference}.usr = NEW.id
AND {artefact_config}.plugin = 'file'
AND {artefact_config}.field = 'quotanotifylimit'
AND NEW.quotaused/NEW.quota < {artefact_config}.value/100;"
);
}
}
......
......@@ -1376,9 +1376,7 @@ function xmldb_core_upgrade($oldversion=0) {
WHERE id IN (
SELECT u.id FROM {usr} u
JOIN {auth_instance} ui ON ui.id = u.authinstance
WHERE ui.authname != 'internal' AND ui.active = 1
)
AND id != 0"); // Ignore the root user
WHERE ui.authname != 'internal' AND ui.active = 1 AND u.id != 0)"); // Ignore the root user
}
}
......@@ -1485,5 +1483,22 @@ function xmldb_core_upgrade($oldversion=0) {
}
}
if ($oldversion < 2019093000) {
$searches = plugins_installed('search');
if (isset($searches['elasticsearch'])) {
log_debug('Remove triggers for elasticsearch');
safe_require('search', 'elasticsearch');
ElasticSearchIndexing::drop_trigger_functions();
}
log_debug('Remove triggers for notifications');
db_drop_trigger('update_unread_insert', 'notification_internal_activity');
db_drop_trigger('update_unread_update', 'notification_internal_activity');
db_drop_trigger('update_unread_delete', 'notification_internal_activity');
db_drop_trigger('update_unread_insert2', 'module_multirecipient_userrelation');
db_drop_trigger('update_unread_update2', 'module_multirecipient_userrelation');
db_drop_trigger('update_unread_delete2', 'module_multirecipient_userrelation');
db_drop_trigger('unmark_quota_exceed_upd_usr_set', 'usr');
}
return $status;
}
......@@ -132,10 +132,11 @@ function column_collation_is_default($table, $column) {
* @uses $db
* @param string $command The sql string you wish to be executed.
* @param array $values When using prepared statements, this is the value array (optional).
* @param string $use_trigger Allow the sql query to do fall through to pseudo_trigger
* @return boolean
* @throws SQLException
*/
function execute_sql($command, array $values=null) {
function execute_sql($command, array $values=null, $use_trigger=true) {
global $db;
if (!is_a($db, 'ADOConnection')) {
......@@ -143,6 +144,28 @@ function execute_sql($command, array $values=null) {
}
$command = db_quote_table_placeholders($command);
list($sqltype, $table, $idsql, $bindoffset) = get_table_from_query($command);
if (!is_null($sqltype) && !is_null($table) && !is_null($idsql)) {
if ($use_trigger && $type = table_need_trigger($table)) {
// Need to find the ids
if (!empty($values) && is_array($values) && count($values) > 0) {
$bindvals = array_slice($values, $bindoffset);
$ids = get_records_sql_array($idsql, $bindvals);
}
else {
$ids = get_records_sql_array($idsql, array());
}
if (!empty($ids)) {
foreach ($ids as $k => $v) {
$v->table = $table;
}
}
if ($type == 'es') {
safe_require('search', 'elasticsearch');
ElasticsearchIndexing::bulk_add_to_queue($ids);
}
}
}
try {
if (!empty($values) && is_array($values) && count($values) > 0) {
......@@ -939,15 +962,44 @@ function set_field_select($table, $newfield, $newvalue, $select, array $values)
$select = db_quote_table_placeholders($select);
$calllocal = false;
if ($type = table_need_trigger($table)) {
// Need to find the ids
$sql = "SELECT *, '" . $table . "' AS " . db_quote_identifier('table') . " FROM " . db_table_name($table) . ' ' . $select;
$ids = get_records_sql_array($sql, $values);
if ($type == 'es') {
safe_require('search', 'elasticsearch');
ElasticsearchIndexing::bulk_add_to_queue($ids);
}
else if ($type == 'local') {
$calllocal = true;
}
}
$values = array_merge(array($newvalue), $values);
$sql = 'UPDATE '. db_table_name($table) .' SET '. db_quote_identifier($newfield) .' = ? ' . $select;
try {
$stmt = $db->Prepare($sql);
return $db->Execute($stmt, $values);
$dbex = $db->Execute($stmt, $values);
}
catch (ADODB_Exception $e) {
throw new SQLException(create_sql_exception_message($e, $sql, $values));
}
if ($calllocal) {
// Call the correct function / savetype
$classname = generate_class_name_from_table($table);
list($plugintype, $pluginname) = generate_plugin_type_from_table($table);
if ($plugintype && $pluginname) {
safe_require($plugintype, $pluginname);
if (method_exists($classname, 'pseudo_trigger')) {
foreach ($ids as $id) {
call_static_method($classname, 'pseudo_trigger', $id->id, 'update');
}
}
}
}
return $dbex;
}
......@@ -974,7 +1026,15 @@ function delete_records($table, $field1=null, $value1=null, $field2=null, $value
$select = where_clause_prepared($field1, $field2, $field3);
$values = where_values_prepared($value1, $value2, $value3);
if ($type = table_need_trigger($table)) {
// Need to find the ids
$sql = "SELECT *, '" . $table . "' AS " . db_quote_identifier('table') . " FROM " . db_table_name($table) . ' ' . $select;
$ids = get_records_sql_array($sql, $values);
if ($type == 'es') {
safe_require('search', 'elasticsearch');
ElasticsearchIndexing::bulk_add_to_queue($ids);
}
}
$sql = 'DELETE FROM '. db_table_name($table) . ' ' . $select;
try {
$stmt = $db->Prepare($sql);
......@@ -997,11 +1057,55 @@ function delete_records($table, $field1=null, $value1=null, $field2=null, $value
*/
function delete_records_select($table, $select='', array $values=null) {
if ($select) {
$select = 'WHERE '.$select;
$select = 'WHERE '. $select;
}
return delete_records_sql('DELETE FROM '. db_table_name($table) .' '. $select, $values);
}
/**
* This function works out the name of the table being affected by an INSERT / UPDATE / DELETE command
* And what SELECT query we would need to run to get IDs of the rows that will be affected by the $sql query
* @param string $sql A raw sql query
* @return array containing - the change $type eg insert
- the $table being changed
- the $idsql, which is an SQL command to fetch the ids of the rows that are about to be changed
- the $bindoffset offset of '?' we need to use for the $idsql (a subset of the $values array)
*/
function get_table_from_query($sql) {
$table = $idsql = $type = null;
$sql = preg_replace('#\R+#', ' ', $sql); // make multiline sql into one line
$sql = preg_replace('#\s+#', ' ', $sql); // make multiple spaces into one space
$bindoffset = 0;
if (preg_match('/^DELETE FROM\s(.*?)\s/i', $sql, $matches)) {
$table = trim($matches[1], '"');
$idsql = preg_replace('/^DELETE FROM/', 'SELECT * FROM', $sql);
$type = 'delete';
}
else if (preg_match('/^INSERT INTO\s(.*?)\s/i', $sql, $matches)) {
$table = trim($matches[1], '"');
$idsql = null; // no existing rows being updated
$type = 'insert';
}
else if (preg_match('/^UPDATE\s(.*?)\s.*?IN\s*\(\s*(SELECT.*)\s*\)/i', $sql, $matches)) {
$table = trim($matches[1], '"');
$idsql = $matches[2];
$type = 'update';
}
else if (preg_match('/^UPDATE\s(.*?)\s.*?FROM\s(.*)/i', $sql, $matches)) {
$table = trim($matches[1], '"');
$idsql = 'SELECT * FROM ' . $matches[2];
$type = 'update';
}
else if (preg_match('/^UPDATE\s(.*?)\sSET\s(.*?)WHERE\s(.*)/i', $sql, $matches)) {
$table = trim($matches[1], '"');
$binds = $matches[2];
$bindoffset = preg_match_all('/(?<!\\\)\?/', $binds);
$idsql = 'SELECT * FROM ' . $matches[1] . ' WHERE ' . $matches[3];
$type = 'update';
}
return array($type, $table, $idsql, $bindoffset);
}
/**
* @todo <nigel> This function does nothing delete specific. The functionality
* it has with the $values parameter should be merged with the execute_sql
......@@ -1014,6 +1118,29 @@ function delete_records_sql($sql, array $values=null) {
try {
$result = false;
// Work out table name and get ids
list($sqltype, $table, $idsql, $bindoffset) = get_table_from_query($sql);
if ($sqltype == 'delete' && !is_null($table) && !is_null($idsql)) {
if ($type = table_need_trigger($table)) {
// Need to find the ids
if (!empty($values) && is_array($values) && count($values) > 0) {
$bindvals = array_slice($values, $bindoffset);
$ids = get_records_sql_array($idsql, $bindvals);
}
else {
$ids = get_records_sql_array($idsql, array());
}
if (!empty($ids)) {
foreach ($ids as $k => $v) {
$v->table = $table;
}
}
if ($type == 'es') {
safe_require('search', 'elasticsearch');
ElasticsearchIndexing::bulk_add_to_queue($ids);
}
}
}
if (!empty($values) && is_array($values) && count($values) > 0) {
$stmt = $db->Prepare($sql);
$result = $db->Execute($stmt, $values);
......@@ -1068,7 +1195,7 @@ function insert_record($table, $dataobject, $primarykey=false, $returnpk=false)
$data = (array)$dataobject;
// Pull out data matching these fields
// Pull out data matching these fields
$ddd = array();
foreach ($columns as $column) {
if (isset($data[$column->name])) {
......@@ -1104,40 +1231,80 @@ function insert_record($table, $dataobject, $primarykey=false, $returnpk=false)
catch (ADODB_Exception $e) {
throw new SQLException(create_sql_exception_message($e, $insertSQL, $ddd));
}
if (is_mysql()) {
// Get the id for Mysql here before any other queries are run
$mysqlid = $db->Insert_ID();
}
// If a return ID is not needed then just return true now
if (empty($returnpk)) {
if (empty($returnpk) && !table_need_trigger($table)) {
return true;
}
$replykey = true;
// All es tables have id column so we need to fetch it if not specified
if (table_need_trigger($table) && empty($primarykey)) {
$replykey = false;
$primarykey = 'id';
}
// We already know the record PK if it's been passed explicitly,
// or if we've retrieved it from a sequence (Postgres).
if (!empty($dataobject->{$primarykey})) {
return $dataobject->{$primarykey};
$id = $dataobject->{$primarykey};
}
else {
$id = $mysqlid;
}
pseudo_trigger($table, $dataobject, (integer)$id, 'insert');
return $replykey ? (integer)$id : true;
}
// This only gets triggered with non-Postgres databases
// however we have some postgres fallback in case we failed
// to find the sequence.
$id = $db->Insert_ID();
function table_need_trigger($table) {
if ($dbprefix = get_config('dbprefix')) {
$table = preg_replace('/' . $dbprefix . '/', '', $table);
}
if (defined('SKIP_TRIGGER') && SKIP_TRIGGER === true) {
return false;
}
static $estables;
if (!defined('BEHAT_TEST') && isset($estables) && in_array($table, $estables)) {
return 'es';
}
if (is_postgres()) {
// try to get the primary key based on id
try {
$oidsql = 'SELECT ' . $primarykey . ' FROM '. db_table_name($table) . ' WHERE oid = ' . $id;
$rs = $db->_Execute($oidsql);
if ($rs->RecordCount() == 1) {
return (integer)$rs->fields[0];
}
throw new SQLException('WTF: somehow got more than one record when searching for a primary key');
}
catch (ADODB_Exception $e) {
throw new SQLException("Trying to get pk from oid failed: "
. $e->getMessage() . " sql was $oidsql");
if (!isset($estables) && get_config('searchplugin') == 'elasticsearch') {
$tables = get_config_plugin('search', 'elasticsearch', 'types');
$estables = explode(',', $tables);
$estables[] = 'view_artefact'; // special
if (in_array($table, $estables)) {
return 'es';
}
}
$localtables = array('notification_internal_activity', 'module_multirecipient_userrelation'); // @TODO have the working out of local tables be a function call
if (isset($localtables) && in_array($table, $localtables)) {
return 'local';
}
return false;
}
return (integer)$id;
function pseudo_trigger($table, $data, $id, $savetype = 'insert') {
if ($type = table_need_trigger($table)) {
if ($type == 'es') {
$artefacttype = ($table == 'artefact' && isset($data->artefacttype)) ? $data->artefacttype : null;
safe_require('search', 'elasticsearch');
ElasticsearchIndexing::add_to_queue($id, $table, $artefacttype);
}
else if ($type == 'local') {
// Call the correct function / savetype
$classname = generate_class_name_from_table($table);
list($plugintype, $pluginname) = generate_plugin_type_from_table($table);
if ($plugintype && $pluginname) {
safe_require($plugintype, $pluginname);
if (method_exists($classname, 'pseudo_trigger')) {
call_static_method($classname, 'pseudo_trigger', $id, $savetype);
}
}
}
}
}
/**
......@@ -1315,19 +1482,62 @@ function update_record($table, $dataobject, $where=null, $primarykey=false, $ret
// Run the query
$sql = 'UPDATE '. db_table_name($table) .' SET '. $setclause . ' WHERE ' . $whereclause;
$calllocal = false;
// Work out table name and get ids
list($sqltype, $table, $idsql, $bindoffset) = get_table_from_query($sql);
if ($sqltype == 'update' && !is_null($table) && !is_null($idsql)) {
if ($type = table_need_trigger($table)) {
// Need to find the ids
if (!empty($wherevalues) && is_array($wherevalues) && count($wherevalues) > 0) {
$ids = get_records_sql_array($idsql, $wherevalues);
}
else {
$ids = get_records_sql_array($idsql, array());
}
if (!empty($ids)) {
foreach ($ids as $k => $v) {
$v->table = $table;
}
}
if ($type == 'es') {
safe_require('search', 'elasticsearch');
ElasticsearchIndexing::bulk_add_to_queue($ids);
}
else if ($type == 'local') {
$calllocal = true;
}
}
}
$dbex = false;
try {
$stmt = $db->Prepare($sql);
$rs = $db->Execute($stmt, array_merge($setclausevalues, $wherevalues));
if ($returnpk) {
$primarykey = $primarykey ? $primarykey : 'id';
$returnsql = 'SELECT ' . $primarykey . ' FROM ' . db_table_name($table) . ' WHERE ' . $whereclause;
return get_field_sql($returnsql, $wherevalues);
$dbex = get_field_sql($returnsql, $wherevalues);
}
return true;
$dbex = true;
}
catch (ADODB_Exception $e) {
throw new SQLException(create_sql_exception_message($e, $sql, array_merge($setclausevalues, $wherevalues)));
}
if ($calllocal && !empty($ids)) {
// Call the correct function / savetype
$classname = generate_class_name_from_table($table);
list($plugintype, $pluginname) = generate_plugin_type_from_table($table);
if ($plugintype && $pluginname) {
safe_require($plugintype, $pluginname);
if (method_exists($classname, 'pseudo_trigger')) {
foreach ($ids as $id) {
call_static_method($classname, 'pseudo_trigger', $id->id, 'update');
}
}
}
}
return $dbex;
}
......
......@@ -1825,6 +1825,31 @@ function generate_class_name() {
return 'Plugin' . implode('', array_map('ucfirst', $args));
}
function generate_class_name_from_table() {
$tableargs = func_get_args();
$args = generate_plugin_type_from_table($tableargs[0]);
return 'Plugin' . implode('', array_map('ucfirst', $args));
}
function generate_plugin_type_from_table() {
$tableargs = func_get_args();
$table = $tableargs[0];
if ($prefix = get_config('dbprefix')) {
$table = preg_replace('/' . $prefix . '/', '', $table);
}
$tableargs = explode('_', $table);
if (count($tableargs) > 2) {
$args = array_slice($tableargs, 0, 2);
// Fix for multirecipientnotifications where table name doesn't follow convention
if (isset($args[1]) && $args[1] == 'multirecipient') {
$args[1] = 'multirecipientnotification';
}
}
return array((isset($args[0]) ? $args[0] : null), (isset($args[1]) ? $args[1] : null));
}
function generate_artefact_class_name($type) {
return 'ArtefactType' . ucfirst($type);
}
......
......@@ -16,7 +16,7 @@ $config = new stdClass();
// See https://wiki.mahara.org/wiki/Developer_Area/Version_Numbering_Policy
// For upgrades on stable branches, increment the version by one. On master, use the date.
$config->version = 2019090901;
$config->version = 2019093000;
$config->series = '19.10';
$config->release = '19.10dev';
$config->minupgradefrom = 2017031605;
......
......@@ -119,34 +119,27 @@ class PluginModuleMultirecipientnotification extends PluginModule {
return $tabs;
}
/**
* The pseudo trigger function that should work like how triggers worked before
* But instead of things happening automatically at db level
* we call the command at the dml.php level to have some control over it
* @param string $id The id of the user to update
* @param string $savetype Whether we are doing an insert / update / or delete
* - Note: in this instance of the pseudo_trigger() we don't care about the $savetype
* as we can work out the current state via an SQL query
*/
public static function pseudo_trigger($id, $savetype = 'insert') {
$usr = get_field('module_multirecipient_userrelation', 'usr', 'id', $id);
execute_sql("UPDATE {usr} SET unread = (
SELECT SUM(counts) FROM (
SELECT COUNT(*) AS counts FROM {module_multirecipient_userrelation} WHERE \"role\" = 'recipient' AND \"read\" = ? AND usr = ?
UNION
SELECT COUNT(*) AS counts FROM {notification_internal_activity} WHERE \"read\" = ? AND usr = ?
) AS countsum
) WHERE id = ?", array('0', $usr, 0, $usr, $usr), false);
}
public static function postinst($prevversion) {
if ($prevversion < 20131010) {
// Add triggers to update user unread message count when updating
// module_multirecipient_userrelation
db_create_trigger(
'update_unread_insert2',
'AFTER', 'INSERT', 'module_multirecipient_userrelation', '
IF NEW.role = \'recipient\' AND NEW.read = \'0\' THEN
UPDATE {usr} SET unread = unread + 1 WHERE id = NEW.usr;
END IF;'
);
db_create_trigger(
'update_unread_update2',
'AFTER', 'UPDATE', 'module_multirecipient_userrelation', '
IF OLD.read = \'0\' AND NEW.read = \'1\' AND NEW.role = \'recipient\' THEN
UPDATE {usr} SET unread = unread - 1 WHERE id = NEW.usr;
ELSEIF OLD.read = \'1\' AND NEW.read = \'0\' AND NEW.role = \'recipient\' THEN
UPDATE {usr} SET unread = unread + 1 WHERE id = NEW.usr;
END IF;'
);
db_create_trigger(
'update_unread_delete2',
'AFTER', 'DELETE', 'module_multirecipient_userrelation', '
IF OLD.read = \'0\' AND OLD.role = \'recipient\' THEN
UPDATE {usr} SET unread = unread - 1 WHERE id = OLD.usr;
END IF;'
);
}
}
/**
......
......@@ -66,34 +66,27 @@ class PluginNotificationInternal extends PluginNotification {
return $messageid;
}
/**
* The pseudo trigger function that should work like how triggers worked before
* But instead of things happening automatically at db level
* we call the command at the dml.php level to have some control over it
* @param string $id The id of the user to update
* @param string $savetype Whether we are doing an insert / update / or delete
* - Note: in this instance of the pseudo_trigger() we don't care about the $savetype
* as we can work out the current state via an SQL query
*/
public static function pseudo_trigger($id, $savetype = 'insert') {
$usr = get_field('notification_internal_activity', 'usr', 'id', $id);
execute_sql("UPDATE {usr} SET unread = (
SELECT SUM(counts) FROM (
SELECT COUNT(*) AS counts FROM {module_multirecipient_userrelation} WHERE \"role\" = 'recipient' AND \"read\" = ? AND usr = ?
UNION
SELECT COUNT(*) AS counts FROM {notification_internal_activity} WHERE \"read\" = ? AND usr = ?
) AS countsum
) WHERE id = ?", array(0, $usr, 0, $usr, $usr), false);
}
public static function postinst($prevversion) {
if ($prevversion == 0) {
// Add triggers to update user unread message count when updating
// notification_internal_activity
db_create_trigger(
'update_unread_insert',
'AFTER', 'INSERT', 'notification_internal_activity', '
IF NEW.read = 0 THEN
UPDATE {usr} SET unread = unread + 1 WHERE id = NEW.usr;
END IF;'
);
db_create_trigger(
'update_unread_update',
'AFTER', 'UPDATE', 'notification_internal_activity', '
IF OLD.read = 0 AND NEW.read = 1 THEN
UPDATE {usr} SET unread = unread - 1 WHERE id = NEW.usr;
ELSEIF OLD.read = 1 AND NEW.read = 0 THEN
UPDATE {usr} SET unread = unread + 1 WHERE id = NEW.usr;
END IF;'
);
db_create_trigger(
'update_unread_delete',
'AFTER', 'DELETE', 'notification_internal_activity', '
IF OLD.read = 0 THEN
UPDATE {usr} SET unread = unread - 1 WHERE id = OLD.usr;
END IF;'
);
}
}
public static function get_event_subscriptions() {
......
This diff is collapsed.
......@@ -248,6 +248,24 @@ class BehatAdmin extends BehatBase {
),
),
),
'search' => array (
'elasticsearch' => array(
'indexname' => array(),
'types' => array(