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