APIv4 - Add Address::getCoordinates action
[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 {
e386356d 17 const IDX_PREFIX = 'civicrm_fts_';
fa5bb5cf
TO
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
fa5bb5cf 74 */
e386356d 75 public static function onToggleFts($oldValue, $newValue): void {
38e5457e
TO
76 if (empty($oldValue) && empty($newValue)) {
77 return;
78 }
79
fa5bb5cf
TO
80 $indexer = CRM_Core_InnoDBIndexer::singleton();
81 $indexer->setActive($newValue);
82 $indexer->fixSchemaDifferences();
83 }
84
85 /**
e97c66ff 86 * Indices.
87 *
88 * (string $table => array $indices)
fa5bb5cf
TO
89 *
90 * ex: $indices['civicrm_contact'][0] = array('first_name', 'last_name');
e97c66ff 91 *
92 * @var array
fa5bb5cf
TO
93 */
94 protected $indices;
95
96 /**
97 * @var bool
98 */
99 protected $isActive;
100
7a9ab499
EM
101 /**
102 * Class constructor.
103 *
e97c66ff 104 * @param bool $isActive
105 * @param array $indices
7a9ab499 106 */
fa5bb5cf
TO
107 public function __construct($isActive, $indices) {
108 $this->isActive = $isActive;
109 $this->indices = $this->normalizeIndices($indices);
110 }
111
7a9ab499
EM
112 /**
113 * Fix schema differences.
114 *
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.
118 */
fa5bb5cf 119 public function fixSchemaDifferences() {
fa5bb5cf
TO
120 foreach ($this->indices as $tableName => $ign) {
121 $todoSqls = $this->reconcileIndexSqls($tableName);
122 foreach ($todoSqls as $todoSql) {
123 CRM_Core_DAO::executeQuery($todoSql);
124 }
125 }
126 }
127
128 /**
ca87146b 129 * Determine if an index is expected to exist.
fa5bb5cf
TO
130 *
131 * @param string $table
6a0b768e
TO
132 * @param array $fields
133 * List of field names that must be in the index.
fa5bb5cf
TO
134 * @return bool
135 */
136 public function hasDeclaredIndex($table, $fields) {
137 if (!$this->isActive) {
138 return FALSE;
139 }
140
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)) {
be2fb01f 145 if (array_diff($fields, $idxFields) == []) {
fa5bb5cf
TO
146 return TRUE;
147 }
148 }
149 }
150
151 return FALSE;
152 }
153
154 /**
155 * Get a list of FTS index names that are currently defined in the database.
156 *
157 * @param string $table
bd343266 158 *
a6c01b45
CW
159 * @return array
160 * (string $indexName => string $indexName)
fa5bb5cf 161 */
bd343266
EM
162 public function findActualFtsIndexNames(string $table): array {
163 $dao = CRM_Core_DAO::executeQuery("
164 SELECT index_name as index_name
165 FROM information_Schema.STATISTICS
166 WHERE table_schema = '" . CRM_Core_DAO::getDatabaseName() . "'
167 AND table_name = '$table'
168 AND index_type = 'FULLTEXT'
169 GROUP BY index_name
170 ");
171
be2fb01f 172 $indexNames = [];
fa5bb5cf
TO
173 while ($dao->fetch()) {
174 $indexNames[$dao->index_name] = $dao->index_name;
175 }
176 return $indexNames;
177 }
178
179 /**
180 * Generate a "CREATE INDEX" statement for each desired
181 * FTS index.
182 *
183 * @param $table
ca87146b 184 *
a6c01b45
CW
185 * @return array
186 * (string $indexName => string $sql)
fa5bb5cf 187 */
e386356d 188 public function buildIndexSql($table): array {
518fa0ee
SL
189 // array (string $idxName => string $sql)
190 $sqls = [];
fa5bb5cf
TO
191 if ($this->isActive && isset($this->indices[$table])) {
192 foreach ($this->indices[$table] as $fields) {
193 $name = self::IDX_PREFIX . md5($table . '::' . implode(',', $fields));
bd343266 194 $sqls[$name] = sprintf('CREATE FULLTEXT INDEX %s ON %s (%s)', $name, $table, implode(',', $fields));
fa5bb5cf
TO
195 }
196 }
197 return $sqls;
198 }
199
200 /**
ca87146b 201 * Generate a "DROP INDEX" statement for each existing FTS index.
fa5bb5cf
TO
202 *
203 * @param string $table
ca87146b 204 *
a6c01b45
CW
205 * @return array
206 * (string $idxName => string $sql)
fa5bb5cf
TO
207 */
208 public function dropIndexSql($table) {
be2fb01f 209 $sqls = [];
fa5bb5cf
TO
210 $names = $this->findActualFtsIndexNames($table);
211 foreach ($names as $name) {
212 $sqls[$name] = sprintf("DROP INDEX %s ON %s", $name, $table);
213 }
214 return $sqls;
215 }
216
217 /**
218 * Construct a set of SQL statements which will create (or preserve)
219 * required indices and destroy unneeded indices.
220 *
ca87146b
EM
221 * @param string $table
222 *
fa5bb5cf
TO
223 * @return array
224 */
225 public function reconcileIndexSqls($table) {
226 $buildIndexSqls = $this->buildIndexSql($table);
227 $dropIndexSqls = $this->dropIndexSql($table);
228
229 $allIndexNames = array_unique(array_merge(
230 array_keys($dropIndexSqls),
231 array_keys($buildIndexSqls)
232 ));
233
be2fb01f 234 $todoSqls = [];
fa5bb5cf
TO
235 foreach ($allIndexNames as $indexName) {
236 if (isset($buildIndexSqls[$indexName]) && isset($dropIndexSqls[$indexName])) {
237 // already exists
238 }
239 elseif (isset($buildIndexSqls[$indexName])) {
240 $todoSqls[] = $buildIndexSqls[$indexName];
241 }
242 else {
243 $todoSqls[] = $dropIndexSqls[$indexName];
244 }
245 }
246 return $todoSqls;
247 }
248
249 /**
ca87146b 250 * Put the indices into a normalized format.
fa5bb5cf
TO
251 *
252 * @param $indices
253 * @return array
254 */
255 public function normalizeIndices($indices) {
be2fb01f 256 $result = [];
fa5bb5cf
TO
257 foreach ($indices as $table => $indicesByTable) {
258 foreach ($indicesByTable as $k => $fields) {
259 sort($fields);
260 $result[$table][] = $fields;
261 }
262 }
263 return $result;
264 }
265
266 /**
ca87146b
EM
267 * Setter for isActive.
268 *
6a0b768e 269 * @param bool $isActive
fa5bb5cf
TO
270 */
271 public function setActive($isActive) {
272 $this->isActive = $isActive;
273 }
274
275 /**
ca87146b
EM
276 * Getter for isActive.
277 *
d5cc0fc2 278 * @return bool
fa5bb5cf
TO
279 */
280 public function getActive() {
281 return $this->isActive;
282 }
96025800 283
fa5bb5cf 284}