Merge pull request #4820 from kurund/CRM-15705
[civicrm-core.git] / CRM / Core / InnoDBIndexer.php
CommitLineData
fa5bb5cf
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
fa5bb5cf
TO
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26*/
27
28/**
29 * The InnoDB indexer is responsible for creating and destroying
30 * full-text indices on InnoDB classes.
31 */
32class CRM_Core_InnoDBIndexer {
33 const IDX_PREFIX = "civicrm_fts_";
34
35 /**
36 * @var CRM_Core_InnoDBIndexer
37 */
38 private static $singleton = NULL;
39
a04af6db
TO
40 /**
41 * @param bool $fresh
42 * @return CRM_Core_InnoDBIndexer
43 */
fa5bb5cf
TO
44 public static function singleton($fresh = FALSE) {
45 if ($fresh || self::$singleton === NULL) {
46 $indices = array(
47 'civicrm_address' => array(
48 array('street_address', 'city', 'postal_code')
49 ),
a04af6db
TO
50 'civicrm_activity' => array(
51 array('subject', 'details'),
52 ),
fa5bb5cf
TO
53 'civicrm_contact' => array(
54 array('sort_name', 'nick_name', 'display_name'),
55 ),
a04af6db
TO
56 'civicrm_contribution' => array(
57 array('source', 'amount_level', 'trxn_Id', 'invoice_id'),
58 ),
fa5bb5cf
TO
59 'civicrm_email' => array(
60 array('email')
61 ),
a04af6db
TO
62 'civicrm_membership' => array(
63 array('source'),
64 ),
fa5bb5cf
TO
65 'civicrm_note' => array(
66 array('subject', 'note'),
67 ),
a04af6db
TO
68 'civicrm_participant' => array(
69 array('source', 'fee_level'),
70 ),
fa5bb5cf
TO
71 'civicrm_phone' => array(
72 array('phone'),
73 ),
74 'civicrm_tag' => array(
75 array('name'),
76 ),
77 );
56c2750b 78 $active = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SEARCH_PREFERENCES_NAME, 'enable_innodb_fts', NULL, FALSE);
d3600e95 79 self::$singleton = new self($active, $indices);
fa5bb5cf
TO
80 }
81 return self::$singleton;
82 }
83
84 /**
85 * (Setting Callback)
86 * Respond to changes in the "enable_innodb_fts" setting
87 *
88 * @param bool $oldValue
89 * @param bool $newValue
90 * @param array $metadata Specification of the setting (per *.settings.php)
91 */
92 public static function onToggleFts($oldValue, $newValue, $metadata) {
93 $indexer = CRM_Core_InnoDBIndexer::singleton();
94 $indexer->setActive($newValue);
95 $indexer->fixSchemaDifferences();
96 }
97
98 /**
99 * @var array (string $table => array $indices)
100 *
101 * ex: $indices['civicrm_contact'][0] = array('first_name', 'last_name');
102 */
103 protected $indices;
104
105 /**
106 * @var bool
107 */
108 protected $isActive;
109
110 public function __construct($isActive, $indices) {
111 $this->isActive = $isActive;
112 $this->indices = $this->normalizeIndices($indices);
113 }
114
115 public function fixSchemaDifferences() {
116 // Limitation: This won't pick up stale indices on tables which are not
117 // declared in $this->indices. That's not much of an issue for now b/c
118 // we have a static list of tables.
119 foreach ($this->indices as $tableName => $ign) {
120 $todoSqls = $this->reconcileIndexSqls($tableName);
121 foreach ($todoSqls as $todoSql) {
122 CRM_Core_DAO::executeQuery($todoSql);
123 }
124 }
125 }
126
127 /**
128 * Determine if an index is expected to exist
129 *
130 * @param string $table
131 * @param array $fields list of field names that must be in the index
132 * @return bool
133 */
134 public function hasDeclaredIndex($table, $fields) {
135 if (!$this->isActive) {
136 return FALSE;
137 }
138
139 if (isset($this->indices[$table])) {
140 foreach ($this->indices[$table] as $idxFields) {
141 // TODO determine if $idxFields must be exact match or merely a subset
142 // if (sort($fields) == sort($idxFields)) {
143 if (array_diff($fields, $idxFields) == array()) {
144 return TRUE;
145 }
146 }
147 }
148
149 return FALSE;
150 }
151
152 /**
153 * Get a list of FTS index names that are currently defined in the database.
154 *
155 * @param string $table
156 * @return array (string $indexName => string $indexName)
157 */
158 public function findActualFtsIndexNames($table) {
dde92015 159 $mysqlVersion = CRM_Core_DAO::singleValueQuery('SELECT VERSION()');
160 if (version_compare($mysqlVersion, '5.6', '<')) {
161 // If we're not on 5.6+, then there cannot be any InnoDB FTS indices!
162 // Also: information_schema.innodb_sys_indexes is only available on 5.6+.
163 return array();
164 }
165
fa5bb5cf
TO
166 // Note: this only works in MySQL 5.6, but this whole system is intended to only work in MySQL 5.6
167 $sql = "
168 SELECT i.name as index_name
169 FROM information_schema.innodb_sys_tables t
170 JOIN information_schema.innodb_sys_indexes i USING (table_id)
171 WHERE t.name = concat(database(),'/$table')
172 AND i.name like '" . self::IDX_PREFIX . "%'
173 ";
174 $dao = CRM_Core_DAO::executeQuery($sql);
175 $indexNames = array();
176 while ($dao->fetch()) {
177 $indexNames[$dao->index_name] = $dao->index_name;
178 }
179 return $indexNames;
180 }
181
182 /**
183 * Generate a "CREATE INDEX" statement for each desired
184 * FTS index.
185 *
186 * @param $table
187 * @return array (string $indexName => string $sql)
188 */
189 public function buildIndexSql($table) {
190 $sqls = array(); // array (string $idxName => string $sql)
191 if ($this->isActive && isset($this->indices[$table])) {
192 foreach ($this->indices[$table] as $fields) {
193 $name = self::IDX_PREFIX . md5($table . '::' . implode(',', $fields));
194 $sqls[$name] = sprintf("CREATE FULLTEXT INDEX %s ON %s (%s)", $name, $table, implode(',', $fields));
195 }
196 }
197 return $sqls;
198 }
199
200 /**
201 * Generate a "DROP INDEX" statement for each existing FTS index
202 *
203 * @param string $table
204 * @return array (string $idxName => string $sql)
205 */
206 public function dropIndexSql($table) {
207 $sqls = array();
208 $names = $this->findActualFtsIndexNames($table);
209 foreach ($names as $name) {
210 $sqls[$name] = sprintf("DROP INDEX %s ON %s", $name, $table);
211 }
212 return $sqls;
213 }
214
215 /**
216 * Construct a set of SQL statements which will create (or preserve)
217 * required indices and destroy unneeded indices.
218 *
219 * @param $table
220 * @return array
221 */
222 public function reconcileIndexSqls($table) {
223 $buildIndexSqls = $this->buildIndexSql($table);
224 $dropIndexSqls = $this->dropIndexSql($table);
225
226 $allIndexNames = array_unique(array_merge(
227 array_keys($dropIndexSqls),
228 array_keys($buildIndexSqls)
229 ));
230
231 $todoSqls = array();
232 foreach ($allIndexNames as $indexName) {
233 if (isset($buildIndexSqls[$indexName]) && isset($dropIndexSqls[$indexName])) {
234 // already exists
235 }
236 elseif (isset($buildIndexSqls[$indexName])) {
237 $todoSqls[] = $buildIndexSqls[$indexName];
238 }
239 else {
240 $todoSqls[] = $dropIndexSqls[$indexName];
241 }
242 }
243 return $todoSqls;
244 }
245
246 /**
247 * Put the indices into a normalized format
248 *
249 * @param $indices
250 * @return array
251 */
252 public function normalizeIndices($indices) {
253 $result = array();
254 foreach ($indices as $table => $indicesByTable) {
255 foreach ($indicesByTable as $k => $fields) {
256 sort($fields);
257 $result[$table][] = $fields;
258 }
259 }
260 return $result;
261 }
262
263 /**
264 * @param boolean $isActive
265 */
266 public function setActive($isActive) {
267 $this->isActive = $isActive;
268 }
269
270 /**
271 * @return boolean
272 */
273 public function getActive() {
274 return $this->isActive;
275 }
276}