Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 454 |
|
0.00% |
0 / 37 |
CRAP | |
0.00% |
0 / 1 |
DbUpgrade | |
0.00% |
0 / 454 |
|
0.00% |
0 / 37 |
28392 | |
0.00% |
0 / 1 |
loadSql | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
getAdapter | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setAdapter | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
query | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getTableInfo | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getAllTables | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCharsetAndCollationProblemsForTableColumns | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
getTableStatus | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getCharsetAndCollationProblemsForTable | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
56 | |||
getCharsetAndCollationProblems | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
fixCharsetAndCollationProblems | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
getTableColumns | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getTableConstraints | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
72 | |||
getMissingTables | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
createMissingTables | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
removeDeprecatedColumns | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getMissingColumns | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
explodeFields | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
compareConstraints | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
getMissingConstraints | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
42 | |||
normalizeConstraints | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
compareConstraintActions | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
constraintIsMissing | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getModifiedConstraints | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
90 | |||
defaultMatches | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
56 | |||
nullableMatches | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
typeMatches | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
210 | |||
parseKeysFromCreateTable | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
columnIsMissing | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getDeprecatedColumns | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getModifiedColumns | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
132 | |||
createMissingColumns | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
createMissingConstraints | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
updateModifiedColumns | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
updateModifiedConstraints | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
getModifiedKeys | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
110 | |||
updateModifiedKeys | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | /** |
4 | * VuFind Action Helper - Database upgrade tools |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Controller_Plugins |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org Main Page |
29 | */ |
30 | |
31 | namespace VuFind\Controller\Plugin; |
32 | |
33 | use Laminas\Db\Adapter\Adapter as DbAdapter; |
34 | use Laminas\Db\Metadata\Source\Factory as DbMetadataSourceFactory; |
35 | use Laminas\Mvc\Controller\Plugin\AbstractPlugin; |
36 | |
37 | use function count; |
38 | use function in_array; |
39 | use function is_object; |
40 | |
41 | /** |
42 | * Action helper to perform database upgrades |
43 | * |
44 | * @category VuFind |
45 | * @package Controller_Plugins |
46 | * @author Demian Katz <demian.katz@villanova.edu> |
47 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
48 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
49 | * @link https://vufind.org Main Page |
50 | */ |
51 | class DbUpgrade extends AbstractPlugin |
52 | { |
53 | /** |
54 | * Database commands to generate table |
55 | * |
56 | * @var array |
57 | */ |
58 | protected $dbCommands = []; |
59 | |
60 | /** |
61 | * Database adapter |
62 | * |
63 | * @var DbAdapter |
64 | */ |
65 | protected $adapter; |
66 | |
67 | /** |
68 | * Table metadata |
69 | * |
70 | * @var array |
71 | */ |
72 | protected $tableInfo = false; |
73 | |
74 | /** |
75 | * Deprecated columns, keyed by table name |
76 | * |
77 | * @var array |
78 | */ |
79 | protected $deprecatedColumns = [ |
80 | 'search' => ['folder_id'], |
81 | ]; |
82 | |
83 | /** |
84 | * Given a SQL file, parse it for table creation commands. |
85 | * |
86 | * @param string $file Filename to load. |
87 | * |
88 | * @return void |
89 | */ |
90 | public function loadSql($file) |
91 | { |
92 | $sql = file_get_contents($file); |
93 | $statements = explode(';', $sql); |
94 | |
95 | // Create an array, indexed by table name, of commands necessary to create |
96 | // the keyed table: |
97 | foreach ($statements as $statement) { |
98 | preg_match( |
99 | '/(create\s+table|alter\s+table)\s+([^\s(]+).*/mi', |
100 | $statement, |
101 | $matches |
102 | ); |
103 | if (isset($matches[2])) { |
104 | $table = str_replace('`', '', $matches[2]); |
105 | if (!isset($this->dbCommands[$table])) { |
106 | $this->dbCommands[$table] = []; |
107 | } |
108 | $this->dbCommands[$table][] = $statement; |
109 | } |
110 | } |
111 | } |
112 | |
113 | /** |
114 | * Get the database adapter. |
115 | * |
116 | * @return DbAdapter |
117 | */ |
118 | public function getAdapter() |
119 | { |
120 | if (!is_object($this->adapter)) { |
121 | throw new \Exception('Database adapter not set.'); |
122 | } |
123 | return $this->adapter; |
124 | } |
125 | |
126 | /** |
127 | * Set a database adapter. |
128 | * |
129 | * @param DbAdapter $adapter Adapter to set |
130 | * |
131 | * @return DbUpgrade |
132 | */ |
133 | public function setAdapter($adapter) |
134 | { |
135 | $this->adapter = $adapter; |
136 | return $this; |
137 | } |
138 | |
139 | /** |
140 | * Execute a query. |
141 | * |
142 | * @param string $sql SQL to run |
143 | * @param bool $logsql Should we return the SQL as a string rather than |
144 | * execute it? |
145 | * |
146 | * @return string SQL if $logsql is true, empty string otherwise |
147 | */ |
148 | public function query($sql, $logsql) |
149 | { |
150 | if ($logsql) { |
151 | return rtrim($sql, ';') . ";\n"; |
152 | } else { |
153 | $this->getAdapter()->query($sql, DbAdapter::QUERY_MODE_EXECUTE); |
154 | } |
155 | return ''; |
156 | } |
157 | |
158 | /** |
159 | * Load table metadata. |
160 | * |
161 | * @param bool $reload Force a reload? (Default is false). |
162 | * |
163 | * @return array |
164 | */ |
165 | protected function getTableInfo($reload = false) |
166 | { |
167 | if ($reload || !$this->tableInfo) { |
168 | $metadata = DbMetadataSourceFactory::createSourceFromAdapter( |
169 | $this->getAdapter() |
170 | ); |
171 | $tables = $metadata->getTables(); |
172 | $this->tableInfo = []; |
173 | foreach ($tables as $current) { |
174 | $this->tableInfo[$current->getName()] = $current; |
175 | } |
176 | } |
177 | return $this->tableInfo; |
178 | } |
179 | |
180 | /** |
181 | * Get a list of all tables in the database. |
182 | * |
183 | * @throws \Exception |
184 | * @return array |
185 | */ |
186 | protected function getAllTables() |
187 | { |
188 | return array_keys($this->getTableInfo()); |
189 | } |
190 | |
191 | /** |
192 | * Support method for getCharsetAndCollationProblemsForTable() -- get column |
193 | * details |
194 | * |
195 | * @param string $table Table to check |
196 | * @param string $collation The desired collation |
197 | * |
198 | * @throws \Exception |
199 | * @return array |
200 | */ |
201 | protected function getCharsetAndCollationProblemsForTableColumns( |
202 | $table, |
203 | $collation |
204 | ) { |
205 | $collation = strtolower($collation); |
206 | |
207 | // Get column summary: |
208 | $sql = "SHOW FULL COLUMNS FROM `{$table}`"; |
209 | $results = $this->getAdapter()->query($sql, DbAdapter::QUERY_MODE_EXECUTE); |
210 | |
211 | // Get expected column types: |
212 | // Parse column names out of the CREATE TABLE SQL, which will always be |
213 | // the first entry in the array; we assume the standard mysqldump |
214 | // formatting is used here. |
215 | preg_match_all( |
216 | '/^ `([^`]*)`\s+([^\s,]+)[\t ,]+.*$/m', |
217 | $this->dbCommands[$table][0], |
218 | $matches |
219 | ); |
220 | $expectedTypes = array_combine($matches[1], $matches[2]); |
221 | |
222 | // Load details: |
223 | $retVal = []; |
224 | foreach ($results as $current) { |
225 | // json fields default to utf8mb4_bin, and we only support that: |
226 | if (($expectedTypes[$current->Field] ?? '') === 'json') { |
227 | continue; |
228 | } |
229 | if (!empty($current->Collation)) { |
230 | $normalizedCollation = strtolower($current->Collation); |
231 | if ($normalizedCollation !== $collation) { |
232 | $retVal[$current->Field] = (array)$current; |
233 | } |
234 | } |
235 | } |
236 | return $retVal; |
237 | } |
238 | |
239 | /** |
240 | * Retrieve (and statically cache) table status information. |
241 | * |
242 | * @return array |
243 | */ |
244 | public function getTableStatus() |
245 | { |
246 | static $status = false; |
247 | if (!$status) { |
248 | $status = $this->getAdapter() |
249 | ->query('SHOW TABLE STATUS', DbAdapter::QUERY_MODE_EXECUTE) |
250 | ->toArray(); |
251 | } |
252 | return $status; |
253 | } |
254 | |
255 | /** |
256 | * Check whether the actual table charset and collation match the expected |
257 | * ones; return false if there is no problem, the desired character set and |
258 | * collation otherwise. |
259 | * |
260 | * @param array $table Information about a table (from getTableStatus()) |
261 | * |
262 | * @return bool|string |
263 | */ |
264 | protected function getCharsetAndCollationProblemsForTable($table) |
265 | { |
266 | if (!isset($this->dbCommands[$table['Name']][0])) { |
267 | return false; |
268 | } |
269 | $match = preg_match( |
270 | '/(CHARSET|CHARACTER SET)[\s=]+(utf8(mb4)?)/', |
271 | $this->dbCommands[$table['Name']][0], |
272 | $matches |
273 | ); |
274 | if (!$match) { |
275 | return false; |
276 | } |
277 | $charset = $matches[2]; |
278 | // Check collation: |
279 | $match = preg_match( |
280 | '/COLLATE[\s=]+(\w+)/', |
281 | $this->dbCommands[$table['Name']][0], |
282 | $matches |
283 | ); |
284 | if (!$match) { |
285 | return false; |
286 | } |
287 | $collation = $matches[1]; |
288 | // The table definition does not include character set, but collation must |
289 | // begin with the character set name, so take it from there |
290 | // (See https://dev.mysql.com/doc/refman/8.0/en/show-table-status.html for |
291 | // more information): |
292 | [$tableCharset] = explode('_', $table['Collation']); |
293 | $problemColumns = $this->getCharsetAndCollationProblemsForTableColumns( |
294 | $table['Name'], |
295 | $collation |
296 | ); |
297 | if ( |
298 | strcasecmp($collation, $table['Collation']) !== 0 |
299 | || strcasecmp($charset, $tableCharset) !== 0 |
300 | || !empty($problemColumns) |
301 | ) { |
302 | return compact('charset', 'collation', 'problemColumns'); |
303 | } |
304 | return false; |
305 | } |
306 | |
307 | /** |
308 | * Get information on character set and collation problems. Return value is an |
309 | * associative array of table name => correct character set and collation values. |
310 | * |
311 | * @throws \Exception |
312 | * @return array |
313 | */ |
314 | public function getCharsetAndCollationProblems() |
315 | { |
316 | // Load details: |
317 | $retVal = []; |
318 | foreach ($this->getTableStatus() as $current) { |
319 | if ($problem = $this->getCharsetAndCollationProblemsForTable($current)) { |
320 | $retVal[$current['Name']] = $problem; |
321 | } |
322 | } |
323 | return $retVal; |
324 | } |
325 | |
326 | /** |
327 | * Fix character set and collation problems based on the output of |
328 | * getCharsetAndCollationProblems(). |
329 | * |
330 | * @param array $tables Output of getCharsetAndCollationProblems() |
331 | * @param bool $logsql Should we return the SQL as a string rather than |
332 | * execute it? |
333 | * |
334 | * @throws \Exception |
335 | * @return string SQL if $logsql is true, empty string otherwise |
336 | */ |
337 | public function fixCharsetAndCollationProblems($tables, $logsql = false) |
338 | { |
339 | $sqlcommands = ''; |
340 | foreach ($tables as $table => $newSettings) { |
341 | foreach ($newSettings['problemColumns'] as $column => $details) { |
342 | // Set up default: |
343 | if (null !== $details['Default']) { |
344 | $safeDefault = $this->getAdapter()->getPlatform() |
345 | ->quoteValue($details['Default']); |
346 | $currentDefault = " DEFAULT {$safeDefault}"; |
347 | } else { |
348 | $currentDefault = ''; |
349 | } |
350 | |
351 | // Change column to appropriate character encoding: |
352 | $sql = "ALTER TABLE `$table` MODIFY `$column` " . $details['Type'] |
353 | . ' COLLATE ' . $newSettings['collation'] |
354 | . (strtoupper($details['Null']) == 'NO' ? ' NOT NULL' : '') |
355 | . $currentDefault |
356 | . ';'; |
357 | $sqlcommands .= $this->query($sql, $logsql); |
358 | } |
359 | // Adjust table character set and collation: |
360 | $sql = "ALTER TABLE `$table` CONVERT TO CHARACTER SET" |
361 | . " {$newSettings['charset']} COLLATE {$newSettings['collation']};"; |
362 | $sqlcommands .= $this->query($sql, $logsql); |
363 | } |
364 | return $sqlcommands; |
365 | } |
366 | |
367 | /** |
368 | * Get information on all columns in a table, keyed by column name. |
369 | * |
370 | * @param string $table Table to describe. |
371 | * |
372 | * @throws \Exception |
373 | * @return array |
374 | */ |
375 | protected function getTableColumns($table) |
376 | { |
377 | $info = $this->getTableInfo(); |
378 | $columns = isset($info[$table]) ? $info[$table]->getColumns() : []; |
379 | $retVal = []; |
380 | foreach ($columns as $current) { |
381 | $retVal[strtolower($current->getName())] = $current; |
382 | } |
383 | return $retVal; |
384 | } |
385 | |
386 | /** |
387 | * Get information on all constraints in a table, keyed by type and constraint |
388 | * name. Primary key is double-keyed as ['primary']['primary'] to keep the |
389 | * structure consistent (since primary keys are not explicitly named in the |
390 | * source SQL). |
391 | * |
392 | * @param string $table Table to describe. |
393 | * |
394 | * @throws \Exception |
395 | * @return array |
396 | */ |
397 | protected function getTableConstraints($table) |
398 | { |
399 | $info = $this->getTableInfo(); |
400 | $constraints = isset($info[$table]) ? $info[$table]->getConstraints() : []; |
401 | $retVal = []; |
402 | foreach ($constraints as $current) { |
403 | $fields = [ |
404 | 'fields' => $current->getColumns(), |
405 | 'deleteRule' => $current->getDeleteRule(), |
406 | 'updateRule' => $current->getUpdateRule(), |
407 | ]; |
408 | switch ($current->getType()) { |
409 | case 'FOREIGN KEY': |
410 | $retVal['foreign'][$current->getName()] = $fields; |
411 | break; |
412 | case 'PRIMARY KEY': |
413 | $retVal['primary']['primary'] = $fields; |
414 | break; |
415 | case 'UNIQUE': |
416 | $retVal['unique'][$current->getName()] = $fields; |
417 | break; |
418 | case 'CHECK': |
419 | // We don't get enough information from getConstraints() to |
420 | // handle CHECK constraints, so just ignore them for now: |
421 | break; |
422 | default: |
423 | throw new \Exception( |
424 | 'Unexpected constraint type: ' . $current->getType() |
425 | ); |
426 | break; |
427 | } |
428 | } |
429 | return $retVal; |
430 | } |
431 | |
432 | /** |
433 | * Get a list of missing tables in the database. |
434 | * |
435 | * @throws \Exception |
436 | * @return array |
437 | */ |
438 | public function getMissingTables() |
439 | { |
440 | $tables = $this->getAllTables(); |
441 | $missing = []; |
442 | foreach (array_keys($this->dbCommands) as $table) { |
443 | if (!in_array(trim(strtolower($table)), $tables)) { |
444 | $missing[] = $table; |
445 | } |
446 | } |
447 | |
448 | // If we got this far, no tables need to be added: |
449 | return $missing; |
450 | } |
451 | |
452 | /** |
453 | * Create missing tables based on the output of getMissingTables(). |
454 | * |
455 | * @param array $tables Output of getMissingTables() |
456 | * @param bool $logsql Should we return the SQL as a string rather than |
457 | * execute it? |
458 | * |
459 | * @throws \Exception |
460 | * @return string SQL if $logsql is true, empty string otherwise |
461 | */ |
462 | public function createMissingTables($tables, $logsql = false) |
463 | { |
464 | $sqlcommands = ''; |
465 | foreach ($tables as $table) { |
466 | $sqlcommands .= $this->query($this->dbCommands[$table][0], $logsql); |
467 | } |
468 | return $sqlcommands; |
469 | } |
470 | |
471 | /** |
472 | * Remove deprecated columns based on the output of getDeprecatedColumns(). |
473 | * |
474 | * @param array $details Output of getDeprecatedColumns() |
475 | * @param bool $logsql Should we return the SQL as a string rather than |
476 | * execute it? |
477 | * |
478 | * @throws \Exception |
479 | * @return string SQL if $logsql is true, empty string otherwise |
480 | */ |
481 | public function removeDeprecatedColumns($details, $logsql = false) |
482 | { |
483 | $sqlcommands = ''; |
484 | foreach ($details as $table => $columns) { |
485 | foreach ($columns as $column) { |
486 | $query = "ALTER TABLE `$table` DROP COLUMN `$column`;"; |
487 | $sqlcommands .= $this->query($query, $logsql); |
488 | } |
489 | } |
490 | return $sqlcommands; |
491 | } |
492 | |
493 | /** |
494 | * Get a list of missing columns in the database tables (associative array, |
495 | * key = table name, value = array of missing column definitions). |
496 | * |
497 | * @param array $missingTables List of missing tables |
498 | * |
499 | * @throws \Exception |
500 | * @return array |
501 | */ |
502 | public function getMissingColumns($missingTables = []) |
503 | { |
504 | $missing = []; |
505 | $this->getTableInfo(true); // force reload of table info |
506 | foreach ($this->dbCommands as $table => $sql) { |
507 | // Skip missing tables if we're logging |
508 | if (in_array($table, $missingTables)) { |
509 | continue; |
510 | } |
511 | |
512 | // Parse column names out of the CREATE TABLE SQL, which will always be |
513 | // the first entry in the array; we assume the standard mysqldump |
514 | // formatting is used here. |
515 | preg_match_all('/^ `([^`]*)`.*$/m', $sql[0], $matches); |
516 | $expectedColumns = $matches[1]; |
517 | |
518 | // Create associative array of column name => SQL defining that column |
519 | $columnDefinitions = []; |
520 | foreach ($expectedColumns as $i => $name) { |
521 | // Strip off any comments: |
522 | $parts = explode('--', $matches[0][$i]); |
523 | |
524 | // Fix trailing whitespace/punctuation: |
525 | $columnDefinitions[$name] = trim(trim($parts[0]), ',;'); |
526 | } |
527 | |
528 | // Now check for missing columns and build our return array: |
529 | $actualColumns = array_keys($this->getTableColumns($table)); |
530 | foreach ($expectedColumns as $column) { |
531 | if (!in_array(strtolower($column), $actualColumns)) { |
532 | if (!isset($missing[$table])) { |
533 | $missing[$table] = []; |
534 | } |
535 | $missing[$table][] = $columnDefinitions[$column]; |
536 | } |
537 | } |
538 | } |
539 | return $missing; |
540 | } |
541 | |
542 | /** |
543 | * Given a field list extracted from a MySQL table definition (e.g. `a`,`b`) |
544 | * return an array of fields (e.g. ['a', 'b']). |
545 | * |
546 | * @param string $fields Field list |
547 | * |
548 | * @return array |
549 | */ |
550 | protected function explodeFields($fields) |
551 | { |
552 | return array_map('trim', explode(',', str_replace('`', '', $fields))); |
553 | } |
554 | |
555 | /** |
556 | * Compare expected vs. actual constraints and return an array of SQL |
557 | * clauses required to create the missing constraints. |
558 | * |
559 | * @param array $expected Expected constraints (based on mysql.sql) |
560 | * @param array $actual Actual constraints (pulled from database metadata) |
561 | * |
562 | * @return array |
563 | */ |
564 | protected function compareConstraints($expected, $actual) |
565 | { |
566 | $missing = []; |
567 | foreach ($expected as $type => $constraints) { |
568 | foreach ($constraints as $constraint) { |
569 | $matchFound = false; |
570 | foreach ($actual[$type] ?? [] as $existing) { |
571 | $diffCount = count( |
572 | array_diff($constraint['fields'], $existing['fields']) |
573 | ) + count( |
574 | array_diff($existing['fields'], $constraint['fields']) |
575 | ); |
576 | if ($diffCount == 0) { |
577 | $matchFound = true; |
578 | break; |
579 | } |
580 | } |
581 | if (!$matchFound) { |
582 | $missing[] = trim(rtrim($constraint['sql'], ',')); |
583 | } |
584 | } |
585 | } |
586 | return $missing; |
587 | } |
588 | |
589 | /** |
590 | * Get a list of missing constraints in the database tables (associative array, |
591 | * key = table name, value = array of missing constraint definitions). |
592 | * |
593 | * @param array $missingTables List of missing tables |
594 | * |
595 | * @throws \Exception |
596 | * @return array |
597 | */ |
598 | public function getMissingConstraints($missingTables = []) |
599 | { |
600 | $missing = []; |
601 | foreach ($this->dbCommands as $table => $sql) { |
602 | // Skip missing tables if we're logging |
603 | if (in_array($table, $missingTables)) { |
604 | continue; |
605 | } |
606 | |
607 | // Parse column names out of the CREATE TABLE SQL, which will always be |
608 | // the first entry in the array; we assume the standard mysqldump |
609 | // formatting is used here. |
610 | preg_match_all( |
611 | '/^ PRIMARY KEY \(`([^)]*)`\).*$/m', |
612 | $sql[0], |
613 | $primaryMatches |
614 | ); |
615 | preg_match_all( |
616 | '/^ CONSTRAINT `([^`]+)` FOREIGN KEY \(`([^)]*)`\).*$/m', |
617 | $sql[0], |
618 | $foreignKeyMatches |
619 | ); |
620 | preg_match_all( |
621 | '/^ UNIQUE KEY `([^`]+)`.*\(`([^)]*)`\).*$/m', |
622 | $sql[0], |
623 | $uniqueMatches |
624 | ); |
625 | $expectedConstraints = [ |
626 | 'primary' => [ |
627 | 'primary' => [ |
628 | 'sql' => $primaryMatches[0][0], |
629 | 'fields' => $this->explodeFields($primaryMatches[1][0]), |
630 | ], |
631 | ], |
632 | ]; |
633 | foreach ($uniqueMatches[0] as $i => $sql) { |
634 | $expectedConstraints['unique'][$uniqueMatches[1][$i]] = [ |
635 | 'sql' => $sql, |
636 | 'fields' => $this->explodeFields($uniqueMatches[2][$i]), |
637 | ]; |
638 | } |
639 | foreach ($foreignKeyMatches[0] as $i => $sql) { |
640 | $expectedConstraints['foreign'][$foreignKeyMatches[1][$i]] = [ |
641 | 'sql' => $sql, |
642 | 'fields' => $this->explodeFields($foreignKeyMatches[2][$i]), |
643 | ]; |
644 | } |
645 | |
646 | // Now check for missing columns and build our return array: |
647 | $actualConstraints = $this->getTableConstraints($table); |
648 | |
649 | $mismatches = $this |
650 | ->compareConstraints($expectedConstraints, $actualConstraints); |
651 | if (!empty($mismatches)) { |
652 | $missing[$table] = $mismatches; |
653 | } |
654 | } |
655 | return $missing; |
656 | } |
657 | |
658 | /** |
659 | * Normalize constraint values. |
660 | * |
661 | * @param array $constraints Constraints to normalize |
662 | * |
663 | * @return array |
664 | */ |
665 | protected function normalizeConstraints($constraints) |
666 | { |
667 | foreach (['deleteRule', 'updateRule'] as $key) { |
668 | // NO ACTION and RESTRICT are equivalent in MySQL, but different |
669 | // versions return different values. Here we normalize them to RESTRICT |
670 | // for simplicity/consistency. |
671 | if ($constraints[$key] == 'NO ACTION') { |
672 | $constraints[$key] = 'RESTRICT'; |
673 | } |
674 | } |
675 | return $constraints; |
676 | } |
677 | |
678 | /** |
679 | * Compare expected vs. actual constraint actions and return an array of SQL |
680 | * clauses required to create the modified constraints. |
681 | * |
682 | * @param array $expected Expected constraints (based on mysql.sql) |
683 | * @param array $actual Actual constraints (pulled from database metadata) |
684 | * |
685 | * @return array |
686 | */ |
687 | protected function compareConstraintActions($expected, $actual) |
688 | { |
689 | $modified = []; |
690 | foreach ($expected as $type => $constraints) { |
691 | foreach ($constraints as $name => $constraint) { |
692 | if (!isset($actual[$type][$name])) { |
693 | throw new \Exception( |
694 | "Could not find constraint '$name' in actual constraints" |
695 | ); |
696 | } |
697 | $actualConstr = $this->normalizeConstraints($actual[$type][$name]); |
698 | if ( |
699 | $constraint['deleteRule'] !== $actualConstr['deleteRule'] |
700 | || $constraint['updateRule'] !== $actualConstr['updateRule'] |
701 | ) { |
702 | $modified[$name] = $constraint; |
703 | } |
704 | } |
705 | } |
706 | return $modified; |
707 | } |
708 | |
709 | /** |
710 | * Support method for getModifiedConstraints() -- check if the current constraint |
711 | * is in the missing constraint list so we can avoid modifying something that |
712 | * does not exist. |
713 | * |
714 | * @param string $constraint Column to check |
715 | * @param array $missing Missing constraint list for constraint's table. |
716 | * |
717 | * @return bool |
718 | */ |
719 | public function constraintIsMissing($constraint, $missing) |
720 | { |
721 | foreach ($missing as $current) { |
722 | preg_match('/^\s*CONSTRAINT\s*`([^`]*)`.*$/', $current, $matches); |
723 | if ($constraint == $matches[1]) { |
724 | return true; |
725 | } |
726 | } |
727 | return false; |
728 | } |
729 | |
730 | /** |
731 | * Get a list of modified constraints in the database tables (associative array, |
732 | * key = table name, value = array of modified constraint definitions). |
733 | * |
734 | * @param array $missingTables List of missing tables |
735 | * @param array $missingConstraints List of missing constraints |
736 | * |
737 | * @throws \Exception |
738 | * @return array |
739 | */ |
740 | public function getModifiedConstraints( |
741 | $missingTables = [], |
742 | $missingConstraints = [] |
743 | ) { |
744 | $modified = []; |
745 | foreach ($this->dbCommands as $table => $sql) { |
746 | // Skip missing tables if we're logging |
747 | if (in_array($table, $missingTables)) { |
748 | continue; |
749 | } |
750 | |
751 | $expectedConstraints = []; |
752 | |
753 | // Parse column names out of the CREATE TABLE SQL, which will always be |
754 | // the first entry in the array; we assume the standard mysqldump |
755 | // formatting is used here. |
756 | preg_match_all( |
757 | '/^\s*CONSTRAINT `([^`]+)` FOREIGN KEY \(`([^)]*)`\)(.*)$/m', |
758 | $sql[0], |
759 | $foreignKeyMatches |
760 | ); |
761 | foreach ($foreignKeyMatches[0] as $i => $sql) { |
762 | $fkName = $foreignKeyMatches[1][$i]; |
763 | // Skip constraint if we're logging and it's missing |
764 | if ( |
765 | isset($missingConstraints[$table]) |
766 | && $this->constraintIsMissing( |
767 | $fkName, |
768 | $missingConstraints[$table] |
769 | ) |
770 | ) { |
771 | continue; |
772 | } |
773 | |
774 | $deleteRule = 'RESTRICT'; |
775 | $updateRule = 'RESTRICT'; |
776 | $options = 'RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT'; |
777 | $actions = $foreignKeyMatches[3][$i] ?? ''; |
778 | if (preg_match("/ON DELETE ($options)/", $actions, $matches)) { |
779 | $deleteRule = $matches[1]; |
780 | } |
781 | if (preg_match("/ON UPDATE ($options)/", $actions, $matches)) { |
782 | $updateRule = $matches[1]; |
783 | } |
784 | |
785 | // Fix trailing whitespace/punctuation: |
786 | $sql = trim(trim($sql), ',;'); |
787 | |
788 | $expectedConstraints['foreign'][$fkName] = [ |
789 | 'sql' => $sql, |
790 | 'fields' => $this->explodeFields($foreignKeyMatches[2][$i]), |
791 | 'deleteRule' => $deleteRule, |
792 | 'updateRule' => $updateRule, |
793 | ]; |
794 | } |
795 | |
796 | // Now check for missing columns and build our return array: |
797 | $actualConstraints = $this->getTableConstraints($table); |
798 | |
799 | $mismatches = $this |
800 | ->compareConstraintActions($expectedConstraints, $actualConstraints); |
801 | if (!empty($mismatches)) { |
802 | $modified[$table]['foreign'] = $mismatches; |
803 | } |
804 | } |
805 | return $modified; |
806 | } |
807 | |
808 | /** |
809 | * Given a current row default, return true if the current default matches the |
810 | * one found in the SQL provided as the $sql parameter. Return false if there |
811 | * is a mismatch that will require table structure updates. |
812 | * |
813 | * @param string $currentDefault Object to check |
814 | * @param string $sql SQL to compare against |
815 | * |
816 | * @return bool |
817 | */ |
818 | protected function defaultMatches($currentDefault, $sql) |
819 | { |
820 | // Normalize current default: |
821 | if ($currentDefault && strtoupper($currentDefault) === 'NULL') { |
822 | $currentDefault = null; |
823 | } |
824 | if (null !== $currentDefault) { |
825 | $currentDefault = trim($currentDefault, "'"); |
826 | if (strtoupper($currentDefault) === 'CURRENT_TIMESTAMP()') { |
827 | $currentDefault = 'CURRENT_TIMESTAMP'; |
828 | } |
829 | } |
830 | |
831 | preg_match('/.* DEFAULT (.*)$/', $sql, $matches); |
832 | $expectedDefault = $matches[1] ?? null; |
833 | if (null !== $expectedDefault) { |
834 | $expectedDefault = trim(rtrim($expectedDefault, ','), "'"); |
835 | $expectedDefault = (strtoupper($expectedDefault) == 'NULL') |
836 | ? null : $expectedDefault; |
837 | } |
838 | return $expectedDefault === $currentDefault; |
839 | } |
840 | |
841 | /** |
842 | * Given a current row default, return true if the current nullability matches |
843 | * the one found in the SQL provided as the $sql parameter. Return false if there |
844 | * is a mismatch that will require table structure updates. |
845 | * |
846 | * @param bool $currentNullable Current nullability |
847 | * @param string $sql SQL to compare against |
848 | * |
849 | * @return bool |
850 | */ |
851 | protected function nullableMatches(bool $currentNullable, string $sql): bool |
852 | { |
853 | $expectedNullable = stripos($sql, 'NOT NULL') ? false : true; |
854 | return $expectedNullable === $currentNullable; |
855 | } |
856 | |
857 | /** |
858 | * Given a table column object, return true if the object's type matches the |
859 | * specified $type parameter. Return false if there is a mismatch that will |
860 | * require table structure updates. |
861 | * |
862 | * @param \Laminas\Db\Metadata\Object\ColumnObject $column Object to check |
863 | * @param string $expectedType Type to compare |
864 | * |
865 | * @return bool |
866 | */ |
867 | protected function typeMatches($column, $expectedType) |
868 | { |
869 | // Get base type: |
870 | $type = $column->getDataType(); |
871 | |
872 | // If it's not a blob or a text (which don't have explicit sizes in our SQL), |
873 | // we should see what the character length is, if any: |
874 | if ( |
875 | $type != 'blob' && $type != 'text' && $type !== 'mediumtext' |
876 | && $type != 'longtext' && $type != 'json' |
877 | ) { |
878 | $charLen = $column->getCharacterMaximumLength(); |
879 | if ($charLen) { |
880 | $type .= '(' . $charLen . ')'; |
881 | } |
882 | } |
883 | |
884 | // If it's an integer, the expected type will have a parenthetical value; |
885 | // this is a display width which we can't retrieve using the column metadata |
886 | // object. Since display width is not important to VuFind, we should ignore |
887 | // this factor when comparing things. |
888 | if ( |
889 | $type == 'int' || $type == 'tinyint' || $type == 'smallint' |
890 | || $type == 'mediumint' || $type == 'bigint' |
891 | ) { |
892 | [$expectedType] = explode('(', $expectedType); |
893 | } |
894 | |
895 | // Some versions of MariaDB store json fields as longtext, while MySQL |
896 | // actually has an explicit json type. We need a special case to handle |
897 | // this inconsistency. See: https://mariadb.com/kb/en/json-data-type/ |
898 | return $type == $expectedType |
899 | || ($type === 'longtext' && $expectedType === 'json'); |
900 | } |
901 | |
902 | /** |
903 | * Parse keys from a "create table" statement |
904 | * |
905 | * @param string $createSql Create table statement |
906 | * |
907 | * @return array |
908 | */ |
909 | protected function parseKeysFromCreateTable(string $createSql): array |
910 | { |
911 | $keys = []; |
912 | // Parse key names etc. out of the CREATE TABLE SQL, which will always be |
913 | // the first entry in the array; we assume the standard mysqldump |
914 | // formatting is used here. |
915 | preg_match_all( |
916 | '/^\s*(UNIQUE\s+)?KEY `([^`]+)` \((.+)\),?$/m', |
917 | $createSql, |
918 | $keyMatches |
919 | ); |
920 | foreach (array_keys($keyMatches[0]) as $i) { |
921 | $unique = !empty($keyMatches[1][$i]); |
922 | $name = $keyMatches[2][$i]; |
923 | // Normalize trailing whitespace and spaces after commas: |
924 | $definition = preg_replace('/,\s+`/', ',`', trim($keyMatches[3][$i])); |
925 | |
926 | $keys[$name] = compact('unique', 'definition'); |
927 | } |
928 | return $keys; |
929 | } |
930 | |
931 | /** |
932 | * Support method for getModifiedColumns() -- check if the current column is |
933 | * in the missing column list so we can avoid modifying something that does |
934 | * not exist. |
935 | * |
936 | * @param string $column Column to check |
937 | * @param array $missing Missing column list for column's table. |
938 | * |
939 | * @return bool |
940 | */ |
941 | public function columnIsMissing($column, $missing) |
942 | { |
943 | foreach ($missing as $current) { |
944 | preg_match('/^\s*`([^`]*)`.*$/', $current, $matches); |
945 | if ($column == $matches[1]) { |
946 | return true; |
947 | } |
948 | } |
949 | return false; |
950 | } |
951 | |
952 | /** |
953 | * Get a list of deprecated columns found in the database. |
954 | * |
955 | * @return array |
956 | */ |
957 | public function getDeprecatedColumns() |
958 | { |
959 | $result = []; |
960 | foreach ($this->deprecatedColumns as $table => $columns) { |
961 | $tableData = $this->getTableColumns(($table)); |
962 | foreach ($columns as $column) { |
963 | if (isset($tableData[$column])) { |
964 | $result[$table] = array_merge($result[$table] ?? [], [$column]); |
965 | } |
966 | } |
967 | } |
968 | return $result; |
969 | } |
970 | |
971 | /** |
972 | * Get a list of changed columns in the database tables (associative array, |
973 | * key = table name, value = array of column name => new data type). |
974 | * |
975 | * @param array $missingTables List of missing tables |
976 | * @param array $missingColumns List of missing columns |
977 | * |
978 | * @throws \Exception |
979 | * @return array |
980 | */ |
981 | public function getModifiedColumns( |
982 | $missingTables = [], |
983 | $missingColumns = [] |
984 | ) { |
985 | $modified = []; |
986 | $this->getTableInfo(true); // force reload of table info |
987 | foreach ($this->dbCommands as $table => $sql) { |
988 | // Skip missing tables if we're logging |
989 | if (in_array($table, $missingTables)) { |
990 | continue; |
991 | } |
992 | |
993 | // Parse column names out of the CREATE TABLE SQL, which will always be |
994 | // the first entry in the array; we assume the standard mysqldump |
995 | // formatting is used here. |
996 | preg_match_all( |
997 | '/^ `([^`]*)`\s+([^\s,]+)[\t ,]+.*$/m', |
998 | $sql[0], |
999 | $matches |
1000 | ); |
1001 | $expectedColumns = array_map('strtolower', $matches[1]); |
1002 | $expectedTypes = $matches[2]; |
1003 | |
1004 | // Create associative array of column name => SQL defining that column |
1005 | $columnDefinitions = []; |
1006 | foreach ($expectedColumns as $i => $name) { |
1007 | // Strip off any comments: |
1008 | $parts = explode('--', $matches[0][$i]); |
1009 | |
1010 | // Fix trailing whitespace/punctuation: |
1011 | $columnDefinitions[$name] = trim(trim($parts[0]), ',;'); |
1012 | } |
1013 | |
1014 | // Now check for mismatched types: |
1015 | $actualColumns = $this->getTableColumns($table); |
1016 | foreach ($expectedColumns as $i => $column) { |
1017 | // Skip column if we're logging and it's missing |
1018 | if ( |
1019 | isset($missingColumns[$table]) |
1020 | && $this->columnIsMissing($column, $missingColumns[$table]) |
1021 | ) { |
1022 | continue; |
1023 | } |
1024 | $currentColumn = $actualColumns[$column]; |
1025 | if ( |
1026 | !$this->typeMatches($currentColumn, $expectedTypes[$i]) |
1027 | || !$this->defaultMatches( |
1028 | $currentColumn->getColumnDefault(), |
1029 | $columnDefinitions[$column] |
1030 | ) |
1031 | || !$this->nullableMatches( |
1032 | $currentColumn->getIsNullable(), |
1033 | $columnDefinitions[$column] |
1034 | ) |
1035 | ) { |
1036 | if (!isset($modified[$table])) { |
1037 | $modified[$table] = []; |
1038 | } |
1039 | $modified[$table][] = $columnDefinitions[$column]; |
1040 | } |
1041 | } |
1042 | } |
1043 | return $modified; |
1044 | } |
1045 | |
1046 | /** |
1047 | * Create missing columns based on the output of getMissingColumns(). |
1048 | * |
1049 | * @param array $columns Output of getMissingColumns() |
1050 | * @param bool $logsql Should we return the SQL as a string rather than |
1051 | * execute it? |
1052 | * |
1053 | * @throws \Exception |
1054 | * @return string SQL if $logsql is true, empty string otherwise |
1055 | */ |
1056 | public function createMissingColumns($columns, $logsql = false) |
1057 | { |
1058 | $sqlcommands = ''; |
1059 | foreach ($columns as $table => $sql) { |
1060 | foreach ($sql as $column) { |
1061 | $sqlcommands .= $this->query( |
1062 | "ALTER TABLE `{$table}` ADD COLUMN {$column}", |
1063 | $logsql |
1064 | ); |
1065 | } |
1066 | } |
1067 | return $sqlcommands; |
1068 | } |
1069 | |
1070 | /** |
1071 | * Create missing constraints based on the output of getMissingConstraints(). |
1072 | * |
1073 | * @param array $constraints Output of getMissingConstraints() |
1074 | * @param bool $logsql Should we return the SQL as a string rather than |
1075 | * execute it? |
1076 | * |
1077 | * @throws \Exception |
1078 | * @return string SQL if $logsql is true, empty string otherwise |
1079 | */ |
1080 | public function createMissingConstraints($constraints, $logsql = false) |
1081 | { |
1082 | $sqlcommands = ''; |
1083 | foreach ($constraints as $table => $sql) { |
1084 | foreach ($sql as $constraint) { |
1085 | $sqlcommands .= $this->query( |
1086 | "ALTER TABLE $table ADD {$constraint};", |
1087 | $logsql |
1088 | ); |
1089 | } |
1090 | } |
1091 | return $sqlcommands; |
1092 | } |
1093 | |
1094 | /** |
1095 | * Modify columns based on the output of getModifiedColumns(). |
1096 | * |
1097 | * @param array $columns Output of getModifiedColumns() |
1098 | * @param bool $logsql Should we return the SQL as a string rather than |
1099 | * execute it? |
1100 | * |
1101 | * @throws \Exception |
1102 | * @return string SQL if $logsql is true, empty string otherwise |
1103 | */ |
1104 | public function updateModifiedColumns($columns, $logsql = false) |
1105 | { |
1106 | $sqlcommands = ''; |
1107 | foreach ($columns as $table => $sql) { |
1108 | foreach ($sql as $column) { |
1109 | $sqlcommands .= $this->query( |
1110 | "ALTER TABLE `{$table}` MODIFY COLUMN {$column}", |
1111 | $logsql |
1112 | ); |
1113 | } |
1114 | } |
1115 | return $sqlcommands; |
1116 | } |
1117 | |
1118 | /** |
1119 | * Modify constraints based on the output of getModifiedConstraints(). |
1120 | * |
1121 | * @param array $constraints Output of getModifiedConstraints() |
1122 | * @param bool $logsql Should we return the SQL as a string rather than |
1123 | * execute it? |
1124 | * |
1125 | * @throws \Exception |
1126 | * @return string SQL if $logsql is true, empty string otherwise |
1127 | */ |
1128 | public function updateModifiedConstraints($constraints, $logsql = false) |
1129 | { |
1130 | $sqlcommands = ''; |
1131 | foreach ($constraints as $table => $constraintTypeList) { |
1132 | foreach ($constraintTypeList as $type => $constraintList) { |
1133 | if ('foreign' !== $type) { |
1134 | throw new \Exception( |
1135 | "Unable to handle modification of constraint type '$type'" |
1136 | ); |
1137 | } |
1138 | foreach ($constraintList as $name => $constraint) { |
1139 | $sqlcommands .= $this->query( |
1140 | "ALTER TABLE `{$table}` DROP FOREIGN KEY `{$name}`", |
1141 | $logsql |
1142 | ); |
1143 | $sqlcommands .= $this->query( |
1144 | "ALTER TABLE $table ADD {$constraint['sql']}", |
1145 | $logsql |
1146 | ); |
1147 | } |
1148 | } |
1149 | } |
1150 | return $sqlcommands; |
1151 | } |
1152 | |
1153 | /** |
1154 | * Get a list of modified keys in the database tables (associative array, |
1155 | * key = table name, value = array of modified key definitions). |
1156 | * |
1157 | * @param array $missingTables List of missing tables |
1158 | * |
1159 | * @throws \Exception |
1160 | * @return array |
1161 | */ |
1162 | public function getModifiedKeys(array $missingTables = []): array |
1163 | { |
1164 | $modified = []; |
1165 | foreach ($this->dbCommands as $table => $sql) { |
1166 | // Skip missing tables if we're logging |
1167 | if (in_array($table, $missingTables)) { |
1168 | continue; |
1169 | } |
1170 | |
1171 | $expectedKeys = $this->parseKeysFromCreateTable($sql[0]); |
1172 | |
1173 | $result = $this->getAdapter()->query( |
1174 | "SHOW CREATE TABLE $table", |
1175 | DbAdapter::QUERY_MODE_EXECUTE |
1176 | )->current(); |
1177 | $resultArray = $result ? $result->getArrayCopy() : ['']; |
1178 | $actualCreateSQL = end($resultArray); |
1179 | $actualKeys = $this->parseKeysFromCreateTable($actualCreateSQL); |
1180 | |
1181 | // Create lists of keys to drop and add: |
1182 | $add = []; |
1183 | // Should we want to drop any keys not found in our database definition |
1184 | // so that it would be possible to e.g. drop columns if necessary, the |
1185 | // following line could be used: |
1186 | //$drop = array_diff(array_keys($actualKeys), array_keys($expectedKeys)); |
1187 | $drop = []; |
1188 | foreach ($expectedKeys as $name => $expected) { |
1189 | if (!isset($actualKeys[$name])) { |
1190 | $add[$name] = $expected; |
1191 | } elseif ( |
1192 | $actualKeys[$name]['unique'] !== $expected['unique'] |
1193 | || $actualKeys[$name]['definition'] !== $expected['definition'] |
1194 | ) { |
1195 | $drop[] = $name; |
1196 | $add[$name] = $expected; |
1197 | } |
1198 | } |
1199 | if ($add || $drop) { |
1200 | $modified[$table] = compact('add', 'drop'); |
1201 | } |
1202 | } |
1203 | return $modified; |
1204 | } |
1205 | |
1206 | /** |
1207 | * Update keys based on the output of getModifiedKeys(). |
1208 | * |
1209 | * @param array $tables Output of getModifiedKeys() |
1210 | * @param bool $logsql Should we return the SQL as a string rather than |
1211 | * execute it? |
1212 | * |
1213 | * @throws \Exception |
1214 | * @return string SQL if $logsql is true, empty string otherwise |
1215 | */ |
1216 | public function updateModifiedKeys($tables, $logsql = false) |
1217 | { |
1218 | $sqlcommands = ''; |
1219 | foreach ($tables as $table => $newSettings) { |
1220 | foreach ($newSettings['drop'] as $key) { |
1221 | $sql = "ALTER TABLE `$table` DROP KEY `$key`"; |
1222 | $sqlcommands .= $this->query($sql, $logsql); |
1223 | } |
1224 | foreach ($newSettings['add'] as $keyName => $keyDetails) { |
1225 | $sql = "ALTER TABLE `$table` ADD " |
1226 | . ($keyDetails['unique'] ? 'UNIQUE ' : '') |
1227 | . "KEY `$keyName` ({$keyDetails['definition']})"; |
1228 | $sqlcommands .= $this->query($sql, $logsql); |
1229 | } |
1230 | } |
1231 | return $sqlcommands; |
1232 | } |
1233 | } |