Merge pull request #15558 from eileenmcnaughton/report_page519
[civicrm-core.git] / CRM / Core / InnoDBIndexer.php
CommitLineData
fa5bb5cf
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
fa5bb5cf 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
fa5bb5cf
TO
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
fa5bb5cf
TO
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) {
be2fb01f
CW
46 $indices = [
47 'civicrm_address' => [
48 ['street_address', 'city', 'postal_code'],
49 ],
50 'civicrm_activity' => [
51 ['subject', 'details'],
52 ],
53 'civicrm_contact' => [
54 ['sort_name', 'nick_name', 'display_name'],
55 ],
56 'civicrm_contribution' => [
57 ['source', 'amount_level', 'trxn_Id', 'invoice_id'],
58 ],
59 'civicrm_email' => [
60 ['email'],
61 ],
62 'civicrm_membership' => [
63 ['source'],
64 ],
65 'civicrm_note' => [
66 ['subject', 'note'],
67 ],
68 'civicrm_participant' => [
69 ['source', 'fee_level'],
70 ],
71 'civicrm_phone' => [
72 ['phone'],
73 ],
74 'civicrm_tag' => [
75 ['name'],
76 ],
77 ];
84fb7424 78 $active = Civi::settings()->get('enable_innodb_fts');
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
6a0b768e
TO
90 * @param array $metadata
91 * Specification of the setting (per *.settings.php).
fa5bb5cf
TO
92 */
93 public static function onToggleFts($oldValue, $newValue, $metadata) {
38e5457e
TO
94 if (empty($oldValue) && empty($newValue)) {
95 return;
96 }
97
fa5bb5cf
TO
98 $indexer = CRM_Core_InnoDBIndexer::singleton();
99 $indexer->setActive($newValue);
100 $indexer->fixSchemaDifferences();
101 }
102
103 /**
e97c66ff 104 * Indices.
105 *
106 * (string $table => array $indices)
fa5bb5cf
TO
107 *
108 * ex: $indices['civicrm_contact'][0] = array('first_name', 'last_name');
e97c66ff 109 *
110 * @var array
fa5bb5cf
TO
111 */
112 protected $indices;
113
114 /**
115 * @var bool
116 */
117 protected $isActive;
118
7a9ab499
EM
119 /**
120 * Class constructor.
121 *
e97c66ff 122 * @param bool $isActive
123 * @param array $indices
7a9ab499 124 */
fa5bb5cf
TO
125 public function __construct($isActive, $indices) {
126 $this->isActive = $isActive;
127 $this->indices = $this->normalizeIndices($indices);
128 }
129
7a9ab499
EM
130 /**
131 * Fix schema differences.
132 *
133 * Limitation: This won't pick up stale indices on tables which are not
134 * declared in $this->indices. That's not much of an issue for now b/c
135 * we have a static list of tables.
136 */
fa5bb5cf 137 public function fixSchemaDifferences() {
fa5bb5cf
TO
138 foreach ($this->indices as $tableName => $ign) {
139 $todoSqls = $this->reconcileIndexSqls($tableName);
140 foreach ($todoSqls as $todoSql) {
141 CRM_Core_DAO::executeQuery($todoSql);
142 }
143 }
144 }
145
146 /**
ca87146b 147 * Determine if an index is expected to exist.
fa5bb5cf
TO
148 *
149 * @param string $table
6a0b768e
TO
150 * @param array $fields
151 * List of field names that must be in the index.
fa5bb5cf
TO
152 * @return bool
153 */
154 public function hasDeclaredIndex($table, $fields) {
155 if (!$this->isActive) {
156 return FALSE;
157 }
158
159 if (isset($this->indices[$table])) {
160 foreach ($this->indices[$table] as $idxFields) {
161 // TODO determine if $idxFields must be exact match or merely a subset
162 // if (sort($fields) == sort($idxFields)) {
be2fb01f 163 if (array_diff($fields, $idxFields) == []) {
fa5bb5cf
TO
164 return TRUE;
165 }
166 }
167 }
168
169 return FALSE;
170 }
171
172 /**
173 * Get a list of FTS index names that are currently defined in the database.
174 *
175 * @param string $table
a6c01b45
CW
176 * @return array
177 * (string $indexName => string $indexName)
fa5bb5cf
TO
178 */
179 public function findActualFtsIndexNames($table) {
dde92015 180 $mysqlVersion = CRM_Core_DAO::singleValueQuery('SELECT VERSION()');
181 if (version_compare($mysqlVersion, '5.6', '<')) {
182 // If we're not on 5.6+, then there cannot be any InnoDB FTS indices!
183 // Also: information_schema.innodb_sys_indexes is only available on 5.6+.
be2fb01f 184 return [];
dde92015 185 }
186
fa5bb5cf
TO
187 // Note: this only works in MySQL 5.6, but this whole system is intended to only work in MySQL 5.6
188 $sql = "
189 SELECT i.name as index_name
190 FROM information_schema.innodb_sys_tables t
191 JOIN information_schema.innodb_sys_indexes i USING (table_id)
192 WHERE t.name = concat(database(),'/$table')
193 AND i.name like '" . self::IDX_PREFIX . "%'
194 ";
195 $dao = CRM_Core_DAO::executeQuery($sql);
be2fb01f 196 $indexNames = [];
fa5bb5cf
TO
197 while ($dao->fetch()) {
198 $indexNames[$dao->index_name] = $dao->index_name;
199 }
200 return $indexNames;
201 }
202
203 /**
204 * Generate a "CREATE INDEX" statement for each desired
205 * FTS index.
206 *
207 * @param $table
ca87146b 208 *
a6c01b45
CW
209 * @return array
210 * (string $indexName => string $sql)
fa5bb5cf
TO
211 */
212 public function buildIndexSql($table) {
518fa0ee
SL
213 // array (string $idxName => string $sql)
214 $sqls = [];
fa5bb5cf
TO
215 if ($this->isActive && isset($this->indices[$table])) {
216 foreach ($this->indices[$table] as $fields) {
217 $name = self::IDX_PREFIX . md5($table . '::' . implode(',', $fields));
218 $sqls[$name] = sprintf("CREATE FULLTEXT INDEX %s ON %s (%s)", $name, $table, implode(',', $fields));
219 }
220 }
221 return $sqls;
222 }
223
224 /**
ca87146b 225 * Generate a "DROP INDEX" statement for each existing FTS index.
fa5bb5cf
TO
226 *
227 * @param string $table
ca87146b 228 *
a6c01b45
CW
229 * @return array
230 * (string $idxName => string $sql)
fa5bb5cf
TO
231 */
232 public function dropIndexSql($table) {
be2fb01f 233 $sqls = [];
fa5bb5cf
TO
234 $names = $this->findActualFtsIndexNames($table);
235 foreach ($names as $name) {
236 $sqls[$name] = sprintf("DROP INDEX %s ON %s", $name, $table);
237 }
238 return $sqls;
239 }
240
241 /**
242 * Construct a set of SQL statements which will create (or preserve)
243 * required indices and destroy unneeded indices.
244 *
ca87146b
EM
245 * @param string $table
246 *
fa5bb5cf
TO
247 * @return array
248 */
249 public function reconcileIndexSqls($table) {
250 $buildIndexSqls = $this->buildIndexSql($table);
251 $dropIndexSqls = $this->dropIndexSql($table);
252
253 $allIndexNames = array_unique(array_merge(
254 array_keys($dropIndexSqls),
255 array_keys($buildIndexSqls)
256 ));
257
be2fb01f 258 $todoSqls = [];
fa5bb5cf
TO
259 foreach ($allIndexNames as $indexName) {
260 if (isset($buildIndexSqls[$indexName]) && isset($dropIndexSqls[$indexName])) {
261 // already exists
262 }
263 elseif (isset($buildIndexSqls[$indexName])) {
264 $todoSqls[] = $buildIndexSqls[$indexName];
265 }
266 else {
267 $todoSqls[] = $dropIndexSqls[$indexName];
268 }
269 }
270 return $todoSqls;
271 }
272
273 /**
ca87146b 274 * Put the indices into a normalized format.
fa5bb5cf
TO
275 *
276 * @param $indices
277 * @return array
278 */
279 public function normalizeIndices($indices) {
be2fb01f 280 $result = [];
fa5bb5cf
TO
281 foreach ($indices as $table => $indicesByTable) {
282 foreach ($indicesByTable as $k => $fields) {
283 sort($fields);
284 $result[$table][] = $fields;
285 }
286 }
287 return $result;
288 }
289
290 /**
ca87146b
EM
291 * Setter for isActive.
292 *
6a0b768e 293 * @param bool $isActive
fa5bb5cf
TO
294 */
295 public function setActive($isActive) {
296 $this->isActive = $isActive;
297 }
298
299 /**
ca87146b
EM
300 * Getter for isActive.
301 *
d5cc0fc2 302 * @return bool
fa5bb5cf
TO
303 */
304 public function getActive() {
305 return $this->isActive;
306 }
96025800 307
fa5bb5cf 308}