From 71aebd922402aaa9a283560cca49642ee0bbb48e Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 24 Jul 2017 23:52:59 -0700 Subject: [PATCH] CRM-20958 - Fill in creation and modification times This implements the migration procedure from CRM-20958. To test it, I did these steps: * Create a new build with `4.6` (eg `civibuild create d46`) * Create a new case (Housing Support; case_id=1, activity_id=625-632) * Edit one of the activities (Long-term housing plan). Tweak the subject. * Edit the same activity again. Tweak the subject again. * Create another case (Adult Day Care Referral; case_id=2, activity_id=633-637) * On the CLI, delete the "Open Case" activity. (cv api activity.delete id=633) * Create a new standalone activity (Meeting; "Mess up my history"; act_id=638) * In SQL, delete its log records (`DELETE FROM civicrm_log WHERE entity_table='civicrm_activity' AND entity_id=XXX`) * Create a new standalone activity (Phone Call; "Do some revisions"; act_id=#639) * Edit the activity multiple times * Make a DB snapshot * `civibuild snapshot d46 --snapshot d46-caseactlog` * Create a new build with `master` (eg `civibuild create dcase` or `dmaster`) * On the newer build, load the snapshot and run the upgrade * `civibuild ut dcase /Users/myuser/buildkit/app/snapshot/d46-caseactlog/civi.sql.gz` * Compare results * On old DB, run * `SELECT * FROM civicrm_log WHERE entity_table='civicrm_activity' AND entity_id BETWEEN 625 AND 639 ORDER BY entity_id, id;` * On new DB, run * `SELECT * FROM civicrm_log WHERE entity_table='civicrm_activity' AND entity_id BETWEEN 625 AND 639 ORDER BY entity_id, id;` (should match above) * `SELECT id, created_date, modified_date FROM civicrm_activity WHERE id BETWEEN 625 AND 639;` (should match above) * `SELECT id, created_date, modified_date FROM civicrm_case WHERE id BETWEEN 1 AND 2;` (should match above) Note that the data produced here is a bit dirty, but upgrade procedure commensurate output: * The last true modification in case `#2` was *deleting* the "Open Case" activity. However, there's no *record* of this deletion in the standard schema, so it doesn't influence the `modified_date` of the case. The problem is with the old data and the old tracking scheme -- not relevant to the migration. * Activity `#638` didn't have any log records, so its `created_date` and `modified_date` were left as `NULL`. --- CRM/Upgrade/Incremental/php/FourSeven.php | 130 ++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/CRM/Upgrade/Incremental/php/FourSeven.php b/CRM/Upgrade/Incremental/php/FourSeven.php index 1a7e9da7898..80bad0fb5d2 100644 --- a/CRM/Upgrade/Incremental/php/FourSeven.php +++ b/CRM/Upgrade/Incremental/php/FourSeven.php @@ -421,6 +421,90 @@ public function upgrade_4_7_24($rev) { $this->addTask('CRM-20958 - Add modified_date to civicrm_case', 'addColumn', 'civicrm_case', 'modified_date', "timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'When was the case (or closely related entity) was created or modified or deleted.'"); + $this->addTask('CRM-20958 - Suspend modification tracking during upgrade', 'toggleModificationTracking', FALSE); + + list($minId, $maxId) = CRM_Core_DAO::executeQuery( + "SELECT coalesce(min(id),0), coalesce(max(id),0) FROM civicrm_activity" + )->getDatabaseResult()->fetchRow(); + for ($startId = $minId; $startId <= $maxId; $startId += self::BATCH_SIZE) { + $endId = $startId + self::BATCH_SIZE - 1; + $vars = array(1 => array($startId, 'Int'), 2 => array($endId, 'Int')); + + $title = sprintf('CRM-20958 - Compute civicrm_activity.created_date from civicrm_log (%d => %d)', $startId, $endId); + $sql = 'UPDATE civicrm_activity + SET created_date = (SELECT MIN(l.modified_date) FROM civicrm_log l WHERE l.entity_table ="civicrm_activity" AND civicrm_activity.id = l.entity_id) + WHERE (id BETWEEN %1 AND %2) + AND created_date IS NULL + '; + $this->addTask($title, 'task_executeQuery', $sql, $vars); + + $title = sprintf('CRM-20958 - Compute civicrm_activity.modified_date from civicrm_log (%d => %d)', $startId, $endId); + $sql = 'UPDATE civicrm_activity + SET modified_date = (SELECT MAX(l.modified_date) FROM civicrm_log l WHERE l.entity_table ="civicrm_activity" AND civicrm_activity.id = l.entity_id) + WHERE (id BETWEEN %1 AND %2) + AND modified_date IS NULL'; + + $this->addTask($title, 'task_executeQuery', $sql, $vars); + } + + $openCaseTypeId = CRM_Core_DAO::singleValueQuery( + 'SELECT value FROM civicrm_option_value cov + INNER JOIN civicrm_option_group cog ON cov.option_group_id = cog.id + WHERE cov.name = "Open Case" and cog.name = "activity_type"' + ); + + list($minId, $maxId) = CRM_Core_DAO::executeQuery( + "SELECT coalesce(min(id),0), coalesce(max(id),0) FROM civicrm_case" + )->getDatabaseResult()->fetchRow(); + for ($startId = $minId; $startId <= $maxId; $startId += self::BATCH_SIZE) { + $endId = $startId + self::BATCH_SIZE - 1; + $vars = array(1 => array($startId, 'Int'), 2 => array($endId, 'Int'), 3 => array($openCaseTypeId, 'Int')); + + // CONSIDER: In my local system, the "Open Case" timestamps seem to be more synthetic (:00:00) + // $title = sprintf('CRM-20958 - Compute civicrm_case.created_date from "Open Case" (%d => %d)', $startId, $endId); + // $sql = 'UPDATE civicrm_case + // SET created_date = ( + // SELECT MIN(a.activity_date_time) + // FROM civicrm_case_activity ca + // INNER JOIN civicrm_activity a ON (ca.activity_id = a.id) + // WHERE civicrm_case.id = ca.case_id + // AND a.activity_type_id = %3 + // ) + // WHERE (id BETWEEN %1 AND %2) + // AND created_date IS NULL + //'; + // $this->addTask($title, 'task_executeQuery', $sql, $vars); + + // In case... for some ungodly reason... the 'Open Case' activity was missing... + $title = sprintf('CRM-20958 - Compute civicrm_case.created_date from the activity log (%d => %d)', $startId, $endId); + $sql = 'UPDATE civicrm_case + SET created_date = ( + SELECT MIN(l.modified_date) + FROM civicrm_case_activity ca + INNER JOIN civicrm_log l ON (l.entity_table = "civicrm_activity" AND ca.activity_id = l.entity_id) + WHERE civicrm_case.id = ca.case_id + ) + WHERE (id BETWEEN %1 AND %2) + AND created_date IS NULL + '; + $this->addTask($title, 'task_executeQuery', $sql, $vars); + + $title = sprintf('CRM-20958 - Compute civicrm_case.modified_date from the activity log (%d => %d)', $startId, $endId); + $sql = 'UPDATE civicrm_case + SET modified_date = ( + SELECT MAX(l.modified_date) + FROM civicrm_case_activity ca + INNER JOIN civicrm_log l ON (l.entity_table = "civicrm_activity" AND ca.activity_id = l.entity_id) + WHERE civicrm_case.id = ca.case_id + ) + WHERE (id BETWEEN %1 AND %2) + AND modified_date IS NULL + '; + $this->addTask($title, 'task_executeQuery', $sql, $vars); + } + + $this->addTask('CRM-20958 - Restore modification tracking', 'toggleModificationTracking', TRUE); + return TRUE; } @@ -1217,4 +1301,50 @@ protected function checkImageUploadDir() { return $config->imageUploadDir && $config->imageUploadURL && $check->isDirAccessible($config->imageUploadDir, $config->imageUploadURL); } + /** + * (Queue Task Callback) + * + * Enable or disable automatic updates to the `modified_date` columns. + * + * When autopopulating the modification times, we don't want to fill in the + * current time. + * + * @param CRM_Queue_TaskContext $ctx + * @param bool $enabled + * @param + * + * @return bool + */ + public static function toggleModificationTracking($ctx, $enabled) { + if ($enabled) { + CRM_Core_DAO::executeQuery('ALTER TABLE civicrm_case CHANGE modified_date modified_date TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); + CRM_Core_DAO::executeQuery('ALTER TABLE civicrm_activity CHANGE modified_date modified_date TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); + } + else { + CRM_Core_DAO::executeQuery('ALTER TABLE civicrm_case CHANGE modified_date modified_date TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP'); + CRM_Core_DAO::executeQuery('ALTER TABLE civicrm_activity CHANGE modified_date modified_date TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP'); + } + return TRUE; + } + + /** + * (Queue Task Callback) + * + * Execute a single SQL query. + * + * @param CRM_Queue_TaskContext $ctx + * @param string $sql + * An SQL template. May include tokens `%1`, `%2`, etc. + * @param array $vars + * List of SQL parameters, as used by executeQuery(). + * + * @return bool + * + * @see CRM_Core_DAO::executeQuery + */ + public static function task_executeQuery($ctx, $sql, $vars) { + CRM_Core_DAO::executeQuery($sql, $vars); + return TRUE; + } + }