[ ['street_address', 'city', 'postal_code'], ], 'civicrm_activity' => [ ['subject', 'details'], ], 'civicrm_contact' => [ ['sort_name', 'nick_name', 'display_name'], ], 'civicrm_contribution' => [ ['source', 'amount_level', 'trxn_Id', 'invoice_id'], ], 'civicrm_email' => [ ['email'], ], 'civicrm_membership' => [ ['source'], ], 'civicrm_note' => [ ['subject', 'note'], ], 'civicrm_participant' => [ ['source', 'fee_level'], ], 'civicrm_phone' => [ ['phone'], ], 'civicrm_tag' => [ ['name'], ], ]; $active = Civi::settings()->get('enable_innodb_fts'); self::$singleton = new self($active, $indices); } return self::$singleton; } /** * (Setting Callback) * Respond to changes in the "enable_innodb_fts" setting * * @param bool $oldValue * @param bool $newValue * @param array $metadata * Specification of the setting (per *.settings.php). */ public static function onToggleFts($oldValue, $newValue, $metadata) { if (empty($oldValue) && empty($newValue)) { return; } $indexer = CRM_Core_InnoDBIndexer::singleton(); $indexer->setActive($newValue); $indexer->fixSchemaDifferences(); } /** * Indices. * * (string $table => array $indices) * * ex: $indices['civicrm_contact'][0] = array('first_name', 'last_name'); * * @var array */ protected $indices; /** * @var bool */ protected $isActive; /** * Class constructor. * * @param bool $isActive * @param array $indices */ public function __construct($isActive, $indices) { $this->isActive = $isActive; $this->indices = $this->normalizeIndices($indices); } /** * Fix schema differences. * * Limitation: This won't pick up stale indices on tables which are not * declared in $this->indices. That's not much of an issue for now b/c * we have a static list of tables. */ public function fixSchemaDifferences() { foreach ($this->indices as $tableName => $ign) { $todoSqls = $this->reconcileIndexSqls($tableName); foreach ($todoSqls as $todoSql) { CRM_Core_DAO::executeQuery($todoSql); } } } /** * Determine if an index is expected to exist. * * @param string $table * @param array $fields * List of field names that must be in the index. * @return bool */ public function hasDeclaredIndex($table, $fields) { if (!$this->isActive) { return FALSE; } if (isset($this->indices[$table])) { foreach ($this->indices[$table] as $idxFields) { // TODO determine if $idxFields must be exact match or merely a subset // if (sort($fields) == sort($idxFields)) { if (array_diff($fields, $idxFields) == []) { return TRUE; } } } return FALSE; } /** * Get a list of FTS index names that are currently defined in the database. * * @param string $table * @return array * (string $indexName => string $indexName) */ public function findActualFtsIndexNames($table) { $mysqlVersion = CRM_Core_DAO::singleValueQuery('SELECT VERSION()'); if (version_compare($mysqlVersion, '5.6', '<')) { // If we're not on 5.6+, then there cannot be any InnoDB FTS indices! // Also: information_schema.innodb_sys_indexes is only available on 5.6+. return []; } // Note: this only works in MySQL 5.6, but this whole system is intended to only work in MySQL 5.6 // Note: In MYSQL 8 the Tables have been renamed from INNODB_SYS_TABLES and INNODB_SYS_INDEXES to INNODB_TABLES and INNODB_INDEXES $innodbTable = "innodb_sys_tables"; $innodbIndex = "innodb_sys_indexes"; if (version_compare($mysqlVersion, '8.0', '>=')) { $innodbTable = "innodb_tables"; $innodbIndex = "innodb_indexes"; } $sql = " SELECT i.name as `index_name` FROM information_schema.$innodbTable t JOIN information_schema.$innodbIndex i USING (table_id) WHERE t.name = concat(database(),'/$table') AND i.name like '" . self::IDX_PREFIX . "%' "; $dao = CRM_Core_DAO::executeQuery($sql); $indexNames = []; while ($dao->fetch()) { $indexNames[$dao->index_name] = $dao->index_name; } return $indexNames; } /** * Generate a "CREATE INDEX" statement for each desired * FTS index. * * @param $table * * @return array * (string $indexName => string $sql) */ public function buildIndexSql($table) { // array (string $idxName => string $sql) $sqls = []; if ($this->isActive && isset($this->indices[$table])) { foreach ($this->indices[$table] as $fields) { $name = self::IDX_PREFIX . md5($table . '::' . implode(',', $fields)); $sqls[$name] = sprintf("CREATE FULLTEXT INDEX %s ON %s (%s)", $name, $table, implode(',', $fields)); } } return $sqls; } /** * Generate a "DROP INDEX" statement for each existing FTS index. * * @param string $table * * @return array * (string $idxName => string $sql) */ public function dropIndexSql($table) { $sqls = []; $names = $this->findActualFtsIndexNames($table); foreach ($names as $name) { $sqls[$name] = sprintf("DROP INDEX %s ON %s", $name, $table); } return $sqls; } /** * Construct a set of SQL statements which will create (or preserve) * required indices and destroy unneeded indices. * * @param string $table * * @return array */ public function reconcileIndexSqls($table) { $buildIndexSqls = $this->buildIndexSql($table); $dropIndexSqls = $this->dropIndexSql($table); $allIndexNames = array_unique(array_merge( array_keys($dropIndexSqls), array_keys($buildIndexSqls) )); $todoSqls = []; foreach ($allIndexNames as $indexName) { if (isset($buildIndexSqls[$indexName]) && isset($dropIndexSqls[$indexName])) { // already exists } elseif (isset($buildIndexSqls[$indexName])) { $todoSqls[] = $buildIndexSqls[$indexName]; } else { $todoSqls[] = $dropIndexSqls[$indexName]; } } return $todoSqls; } /** * Put the indices into a normalized format. * * @param $indices * @return array */ public function normalizeIndices($indices) { $result = []; foreach ($indices as $table => $indicesByTable) { foreach ($indicesByTable as $k => $fields) { sort($fields); $result[$table][] = $fields; } } return $result; } /** * Setter for isActive. * * @param bool $isActive */ public function setActive($isActive) { $this->isActive = $isActive; } /** * Getter for isActive. * * @return bool */ public function getActive() { return $this->isActive; } }