* @copyright CiviCRM LLC (c) 2004-2019
*/
class CRM_Logging_Schema {
+
+ /**
+ * Default storage engine for log tables
+ *
+ * @var string
+ */
+ const ENGINE = 'InnoDB';
+
private $logs = [];
private $tables = [];
/**
* Specifications of all log table including
- * - engine (default is archive, if not set.)
+ * - engine (default is InnoDB, if not set.)
* - engine_config, a string appended to the engine type.
* For INNODB space can be saved with 'ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4'
* - indexes (default is none and they cannot be added unless engine is innodb. If they are added and
* and also implements the engine change defined by the hook (i.e. INNODB).
*
* Note changing engine & adding hook-defined indexes, but not changing back
- * to ARCHIVE if engine has not been deliberately set (by hook) and not dropping
- * indexes. Sysadmin will need to manually intervene to revert to defaults.
+ * to INNODB if engine has not been deliberately set (by hook) and not
+ * dropping indexes. Sysadmin will need to manually intervene to revert to
+ * defaults.
*
* @param array $params
- * 'updateChangedEngineConfig' - update if the engine config changes, default FALSE
+ * 'updateChangedEngineConfig' - update if the engine config changes?
+ * 'forceEngineMigration' - force engine upgrade from ARCHIVE to InnoDB?
*
* @return int $updateTablesCount
+ * @throws \CiviCRM_API3_Exception
*/
- public function updateLogTableSchema($params = []) {
- isset($params['updateChangedEngineConfig']) ? NULL : $params['updateChangedEngineConfig'] = FALSE;
-
+ public function updateLogTableSchema($params) {
$updateLogConn = FALSE;
$updatedTablesCount = 0;
foreach ($this->logs as $mainTable => $logTable) {
$alterSql = [];
$tableSpec = $this->logTableSpec[$mainTable];
- $engineChanged = isset($tableSpec['engine']) && (strtoupper($tableSpec['engine']) != $this->getEngineForLogTable($logTable));
+ $currentEngine = strtoupper($this->getEngineForLogTable($logTable));
+ if (!isset($tableSpec['engine']) && $currentEngine == 'ARCHIVE' && $params['forceEngineMigration']) {
+ // table uses ARCHIVE engine (the previous default) and no one set an
+ // alternative engine via hook_civicrm_alterLogTables => force change to
+ // new default
+ $tableSpec['engine'] = self::ENGINE;
+ }
+ $engineChanged = isset($tableSpec['engine']) && (strtoupper($tableSpec['engine']) != $currentEngine);
$engineConfigChanged = isset($tableSpec['engine_config']) && (strtoupper($tableSpec['engine_config']) != $this->getEngineConfigForLogTable($logTable));
if ($engineChanged || ($engineConfigChanged && $params['updateChangedEngineConfig'])) {
$alterSql[] = "ENGINE=" . $tableSpec['engine'] . " " . CRM_Utils_Array::value('engine_config', $tableSpec);
* name of the relevant table.
* @param array $cols
* Mixed array of columns to add or null (to check for the missing columns).
- * @param bool $rebuildTrigger
- * should we rebuild the triggers.
*
* @return bool
*/
- public function fixSchemaDifferencesFor($table, $cols = [], $rebuildTrigger = FALSE) {
+ public function fixSchemaDifferencesFor($table, $cols = []) {
if (empty($table)) {
return FALSE;
}
$cols = $this->columnsWithDiffSpecs($table, "log_$table");
}
+ // If a column that already exists on logging table is being added, we
+ // should treat it as a modification.
+ $this->resetSchemaCacheForTable("log_$table");
+ $logTableSchema = $this->columnSpecsOf("log_$table");
+ foreach ($cols['ADD'] as $colKey => $col) {
+ if (array_key_exists($col, $logTableSchema)) {
+ $cols['MODIFY'][] = $col;
+ unset($cols['ADD'][$colKey]);
+ }
+ }
+
// use the relevant lines from CREATE TABLE to add colums to the log table
$create = $this->_getCreateQuery($table);
foreach ((['ADD', 'MODIFY']) as $alterType) {
}
}
- if ($rebuildTrigger) {
- // invoke the meta trigger creation call
- CRM_Core_DAO::triggerRebuild($table);
- }
+ $this->resetSchemaCacheForTable("log_$table");
+
return TRUE;
}
+ /**
+ * Resets schema cache for the given table.
+ *
+ * @param string $table
+ * Name of the table.
+ */
+ private function resetSchemaCacheForTable($table) {
+ unset(\Civi::$statics[__CLASS__]['columnSpecs'][$table]);
+ }
+
/**
* Get query table.
*
*/
public function fixSchemaDifferencesForAll($rebuildTrigger = FALSE) {
$diffs = [];
+ $this->resetTableColumnsCache();
+
foreach ($this->tables as $table) {
if (empty($this->logs[$table])) {
$this->createLogTableFor($table);
}
foreach ($diffs as $table => $cols) {
- $this->fixSchemaDifferencesFor($table, $cols, FALSE);
+ $this->fixSchemaDifferencesFor($table, $cols);
}
if ($rebuildTrigger) {
// invoke the meta trigger creation call
}
}
+ /**
+ * Resets columnSpecs.
+ *
+ * Resets columnSpecs static array in Civi's $statics to make sure we use the
+ * real state of the schema to perform sync operations between core and
+ * logging tables.
+ */
+ private function resetTableColumnsCache() {
+ unset(\Civi::$statics[__CLASS__]['columnSpecs']);
+ }
+
/**
* Fix timestamp.
*
}
\Civi::$statics[__CLASS__]['columnsOf'][$table] = [];
while ($dao->fetch()) {
- \Civi::$statics[__CLASS__]['columnsOf'][$table][] = CRM_Utils_type::escape($dao->Field, 'MysqlColumnNameOrAlias');
+ \Civi::$statics[__CLASS__]['columnsOf'][$table][] = CRM_Utils_Type::escape($dao->Field, 'MysqlColumnNameOrAlias');
}
}
return \Civi::$statics[__CLASS__]['columnsOf'][$table];
// - prepend the name with log_
// - drop AUTO_INCREMENT columns
// - drop non-column rows of the query (keys, constraints, etc.)
- // - set the ENGINE to the specified engine (default is archive or if archive is disabled or nor installed INNODB)
+ // - set the ENGINE to the specified engine (default is INNODB)
// - add log-specific columns (at the end of the table)
$mysqlEngines = [];
$engines = CRM_Core_DAO::executeQuery("SHOW ENGINES");
$mysqlEngines[] = $engines->Engine;
}
}
- $logEngine = in_array('ARCHIVE', $mysqlEngines) ? 'ARCHIVE' : 'INNODB';
$query = preg_replace("/^CREATE TABLE `$table`/i", "CREATE TABLE `{$this->db}`.log_$table", $query);
$query = preg_replace("/ AUTO_INCREMENT/i", '', $query);
$query = preg_replace("/^ [^`].*$/m", '', $query);
- $engine = strtoupper(CRM_Utils_Array::value('engine', $this->logTableSpec[$table], $logEngine));
+ $engine = strtoupper(CRM_Utils_Array::value('engine', $this->logTableSpec[$table], self::ENGINE));
$engine .= " " . CRM_Utils_Array::value('engine_config', $this->logTableSpec[$table]);
$query = preg_replace("/^\) ENGINE=[^ ]+ /im", ') ENGINE=' . $engine . ' ', $query);
// logging is enabled, so now lets create the trigger info tables
foreach ($tableNames as $table) {
+ if (!isset($this->logTableSpec[$table])) {
+ // Per testIgnoreCustomTableByHook this would be unset if a hook had
+ // intervened to prevent logging / triggers on this table.
+ // This could go to the extent of blocking the updates to 'modified_date'
+ // which makes sense, in particular, for calculated fields.
+ continue;
+ }
$columns = $this->columnsOf($table, $force);
// only do the change if any data has changed