d869076723ed27b9ce77a1963233251c43b5b470
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
13 * The InnoDB indexer is responsible for creating and destroying
14 * full-text indices on InnoDB classes.
16 class CRM_Core_InnoDBIndexer
{
17 const IDX_PREFIX
= 'civicrm_fts_';
20 * @var CRM_Core_InnoDBIndexer
22 private static $singleton = NULL;
26 * @return CRM_Core_InnoDBIndexer
28 public static function singleton($fresh = FALSE) {
29 if ($fresh || self
::$singleton === NULL) {
31 'civicrm_address' => [
32 ['street_address', 'city', 'postal_code'],
34 'civicrm_activity' => [
35 ['subject', 'details'],
37 'civicrm_contact' => [
38 ['sort_name', 'nick_name', 'display_name'],
40 'civicrm_contribution' => [
41 ['source', 'amount_level', 'trxn_Id', 'invoice_id'],
46 'civicrm_membership' => [
52 'civicrm_participant' => [
53 ['source', 'fee_level'],
62 $active = Civi
::settings()->get('enable_innodb_fts');
63 self
::$singleton = new self($active, $indices);
65 return self
::$singleton;
70 * Respond to changes in the "enable_innodb_fts" setting
72 * @param bool $oldValue
73 * @param bool $newValue
75 public static function onToggleFts($oldValue, $newValue): void
{
76 if (empty($oldValue) && empty($newValue)) {
80 $indexer = CRM_Core_InnoDBIndexer
::singleton();
81 $indexer->setActive($newValue);
82 $indexer->fixSchemaDifferences();
88 * (string $table => array $indices)
90 * ex: $indices['civicrm_contact'][0] = array('first_name', 'last_name');
104 * @param bool $isActive
105 * @param array $indices
107 public function __construct($isActive, $indices) {
108 $this->isActive
= $isActive;
109 $this->indices
= $this->normalizeIndices($indices);
113 * Fix schema differences.
115 * Limitation: This won't pick up stale indices on tables which are not
116 * declared in $this->indices. That's not much of an issue for now b/c
117 * we have a static list of tables.
119 public function fixSchemaDifferences() {
120 foreach ($this->indices
as $tableName => $ign) {
121 $todoSqls = $this->reconcileIndexSqls($tableName);
122 foreach ($todoSqls as $todoSql) {
123 CRM_Core_DAO
::executeQuery($todoSql);
129 * Determine if an index is expected to exist.
131 * @param string $table
132 * @param array $fields
133 * List of field names that must be in the index.
136 public function hasDeclaredIndex($table, $fields) {
137 if (!$this->isActive
) {
141 if (isset($this->indices
[$table])) {
142 foreach ($this->indices
[$table] as $idxFields) {
143 // TODO determine if $idxFields must be exact match or merely a subset
144 // if (sort($fields) == sort($idxFields)) {
145 if (array_diff($fields, $idxFields) == []) {
155 * Get a list of FTS index names that are currently defined in the database.
157 * @param string $table
159 * (string $indexName => string $indexName)
161 public function findActualFtsIndexNames($table) {
162 $mysqlVersion = CRM_Core_DAO
::singleValueQuery('SELECT VERSION()');
163 // Note: In MYSQL 8 the Tables have been renamed from INNODB_SYS_TABLES and INNODB_SYS_INDEXES to INNODB_TABLES and INNODB_INDEXES
164 $innodbTable = 'innodb_sys_tables';
165 $innodbIndex = "innodb_sys_indexes";
166 if (version_compare($mysqlVersion, '8.0', '>=')
167 // As of 10.4 mariadb is NOT adopting the mysql 8 table names
168 // - this means it's likely it never will.
169 && stripos($mysqlVersion, 'mariadb') === FALSE) {
170 $innodbTable = 'innodb_tables';
171 $innodbIndex = 'innodb_indexes';
174 SELECT i.name as `index_name`
175 FROM information_schema.$innodbTable t
176 JOIN information_schema.$innodbIndex i USING (table_id)
177 WHERE t.name = concat(database(),'/$table')
178 AND i.name like '" . self
::IDX_PREFIX
. "%'
180 $dao = CRM_Core_DAO
::executeQuery($sql);
182 while ($dao->fetch()) {
183 $indexNames[$dao->index_name
] = $dao->index_name
;
189 * Generate a "CREATE INDEX" statement for each desired
195 * (string $indexName => string $sql)
197 public function buildIndexSql($table): array {
198 // array (string $idxName => string $sql)
200 if ($this->isActive
&& isset($this->indices
[$table])) {
201 foreach ($this->indices
[$table] as $fields) {
202 $name = self
::IDX_PREFIX
. md5($table . '::' . implode(',', $fields));
203 $sqls[$name] = sprintf("CREATE FULLTEXT INDEX %s ON %s (%s)", $name, $table, implode(',', $fields));
210 * Generate a "DROP INDEX" statement for each existing FTS index.
212 * @param string $table
215 * (string $idxName => string $sql)
217 public function dropIndexSql($table) {
219 $names = $this->findActualFtsIndexNames($table);
220 foreach ($names as $name) {
221 $sqls[$name] = sprintf("DROP INDEX %s ON %s", $name, $table);
227 * Construct a set of SQL statements which will create (or preserve)
228 * required indices and destroy unneeded indices.
230 * @param string $table
234 public function reconcileIndexSqls($table) {
235 $buildIndexSqls = $this->buildIndexSql($table);
236 $dropIndexSqls = $this->dropIndexSql($table);
238 $allIndexNames = array_unique(array_merge(
239 array_keys($dropIndexSqls),
240 array_keys($buildIndexSqls)
244 foreach ($allIndexNames as $indexName) {
245 if (isset($buildIndexSqls[$indexName]) && isset($dropIndexSqls[$indexName])) {
248 elseif (isset($buildIndexSqls[$indexName])) {
249 $todoSqls[] = $buildIndexSqls[$indexName];
252 $todoSqls[] = $dropIndexSqls[$indexName];
259 * Put the indices into a normalized format.
264 public function normalizeIndices($indices) {
266 foreach ($indices as $table => $indicesByTable) {
267 foreach ($indicesByTable as $k => $fields) {
269 $result[$table][] = $fields;
276 * Setter for isActive.
278 * @param bool $isActive
280 public function setActive($isActive) {
281 $this->isActive
= $isActive;
285 * Getter for isActive.
289 public function getActive() {
290 return $this->isActive
;