Fix package check in update_com.woltlab.wcf_5.4_removeFiles.php
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / database / editor / MySQLDatabaseEditor.class.php
CommitLineData
11ade432
AE
1<?php
2namespace wcf\system\database\editor;
bf64fe54 3use wcf\system\database\exception\DatabaseQueryExecutionException;
770f5de3 4use wcf\system\database\util\PreparedStatementConditionBuilder;
82a7de7f 5use wcf\system\Regex;
11ade432
AE
6
7/**
a17de04e
MS
8 * Database editor implementation for MySQL4.1 or higher.
9 *
11ade432 10 * @author Marcel Werk
7b7b9764 11 * @copyright 2001-2019 WoltLab GmbH
11ade432 12 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
e71525e4 13 * @package WoltLabSuite\Core\System\Database\Editor
11ade432
AE
14 */
15class MySQLDatabaseEditor extends DatabaseEditor {
16 /**
0fcfe5f6 17 * @inheritDoc
11ade432
AE
18 */
19 public function getTableNames() {
058cbd6a 20 $existingTables = [];
11ade432
AE
21 $sql = "SHOW TABLES FROM `".$this->dbObj->getDatabaseName()."`";
22 $statement = $this->dbObj->prepareStatement($sql);
23 $statement->execute();
24 while ($row = $statement->fetchArray(\PDO::FETCH_NUM)) {
25 $existingTables[] = $row[0];
26 }
27 return $existingTables;
28 }
29
30 /**
0fcfe5f6 31 * @inheritDoc
11ade432
AE
32 */
33 public function getColumns($tableName) {
058cbd6a 34 $columns = [];
ef5d8ae6 35 $regex = new Regex('([a-z]+)\((.+)\)', Regex::CASE_INSENSITIVE);
77bb36ca 36
4f2c4eb8 37 $sql = "SHOW COLUMNS FROM `".$tableName."`";
11ade432
AE
38 $statement = $this->dbObj->prepareStatement($sql);
39 $statement->execute();
40 while ($row = $statement->fetchArray()) {
14d48464 41 $regex->match($row['Type']);
42 $typeMatches = $regex->getMatches();
77bb36ca 43
4d2bd2ec
MS
44 $type = $row['Type'];
45 $length = '';
46 $decimals = '';
47 $enumValues = '';
48 if (!empty($typeMatches)) {
49 $type = $typeMatches[1];
50
51 switch ($type) {
52 case 'enum':
53 case 'set':
54 $enumValues = $typeMatches[2];
55 break;
56
57 case 'decimal':
58 case 'double':
59 case 'float':
60 $pieces = explode(',', $typeMatches[2]);
61 switch (count($pieces)) {
62 case 1:
63 $length = $pieces[0];
64 break;
65
66 case 2:
67 list($length, $decimals) = $pieces;
68 break;
69 }
70
71 break;
72
73 default:
74 if ($typeMatches[2] == (int)$typeMatches[2]) {
75 $length = $typeMatches[2];
76 }
77 break;
78 }
79 }
80
058cbd6a 81 $columns[] = ['name' => $row['Field'], 'data' => [
4d2bd2ec
MS
82 'type' => $type,
83 'length' => $length,
84 'notNull' => $row['Null'] == 'YES' ? false : true,
63b9817b 85 'key' => ($row['Key'] == 'PRI') ? 'PRIMARY' : (($row['Key'] == 'UNI') ? 'UNIQUE' : ''),
5fe135db 86 'default' => $row['Default'],
4d2bd2ec
MS
87 'autoIncrement' => $row['Extra'] == 'auto_increment' ? true : false,
88 'enumValues' => $enumValues,
ef5d8ae6 89 'decimals' => $decimals
058cbd6a 90 ]];
ebe8e825 91 }
bf27dc91 92
9f959ced 93 return $columns;
11ade432
AE
94 }
95
dc62342b
MS
96 /**
97 * @inheritDoc
98 */
99 public function getForeignKeys($tableName) {
770f5de3
MS
100 $sql = "SELECT CONSTRAINT_NAME, DELETE_RULE, UPDATE_RULE
101 FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
102 WHERE CONSTRAINT_SCHEMA = ?
103 AND TABLE_NAME = ?";
dc62342b
MS
104 $statement = $this->dbObj->prepareStatement($sql);
105 $statement->execute([
106 $this->dbObj->getDatabaseName(),
107 $tableName
108 ]);
770f5de3 109 $referentialConstraints = $statement->fetchAll(\PDO::FETCH_ASSOC);
dc62342b
MS
110
111 $validActions = ['CASCADE', 'SET NULL', 'NO ACTION'];
112
113 $foreignKeys = [];
770f5de3
MS
114 foreach ($referentialConstraints as $information) {
115 $foreignKeys[$information['CONSTRAINT_NAME']] = [
116 'columns' => [],
117 'referencedColumns' => [],
118 'ON DELETE' => in_array($information['DELETE_RULE'], $validActions) ? $information['DELETE_RULE'] : null,
119 'ON UPDATE' => in_array($information['UPDATE_RULE'], $validActions) ? $information['UPDATE_RULE'] : null
120 ];
121 }
122
123 if (empty($foreignKeys)) {
124 return [];
125 }
126
127 $conditionBuilder = new PreparedStatementConditionBuilder();
128 $conditionBuilder->add('CONSTRAINT_NAME IN (?)', [array_keys($foreignKeys)]);
129 $conditionBuilder->add('TABLE_SCHEMA = ?', [$this->dbObj->getDatabaseName()]);
130 $conditionBuilder->add('TABLE_NAME = ?', [$tableName]);
131
132 $sql = "SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
133 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
134 " . $conditionBuilder;
135 $statement = $this->dbObj->prepareStatement($sql);
136 $statement->execute($conditionBuilder->getParameters());
137 $keyColumnUsage = $statement->fetchAll(\PDO::FETCH_ASSOC);
138
139 foreach ($keyColumnUsage as $information) {
140 $foreignKeys[$information['CONSTRAINT_NAME']]['columns'][] = $information['COLUMN_NAME'];
141 $foreignKeys[$information['CONSTRAINT_NAME']]['referencedColumns'][] = $information['REFERENCED_COLUMN_NAME'];
142 $foreignKeys[$information['CONSTRAINT_NAME']]['referencedTable'] = $information['REFERENCED_TABLE_NAME'];
dc62342b
MS
143 }
144
145 foreach ($foreignKeys as $keyName => $keyData) {
146 $foreignKeys[$keyName]['columns'] = array_unique($foreignKeys[$keyName]['columns']);
147 $foreignKeys[$keyName]['referencedColumns'] = array_unique($foreignKeys[$keyName]['referencedColumns']);
148 }
149
150 return $foreignKeys;
151 }
152
153 /**
154 * @inheritDoc
155 */
156 public function getIndexInformation($tableName) {
157 $sql = "SHOW INDEX
158 FROM `".$tableName."`";
159 $statement = $this->dbObj->prepareStatement($sql);
160 $statement->execute();
161 $indices = $statement->fetchAll(\PDO::FETCH_ASSOC);
162
163 $indexInformation = [];
164 foreach ($indices as $index) {
165 if (!isset($indexInformation[$index['Key_name']])) {
166 $type = null;
167 if ($index['Index_type'] === 'FULLTEXT') {
168 $type = 'FULLTEXT';
169 }
170 else if ($index['Key_name'] === 'PRIMARY') {
171 $type = 'PRIMARY';
172 }
173 else if ($index['Non_unique'] == 0) {
174 $type = 'UNIQUE';
175 }
176
177 $indexInformation[$index['Key_name']] = [
178 'columns' => [$index['Column_name']],
179 'type' => $type
180 ];
181 }
182 else {
183 $indexInformation[$index['Key_name']]['columns'][] = $index['Column_name'];
184 }
185 }
186
187 return $indexInformation;
188 }
189
11ade432 190 /**
0fcfe5f6 191 * @inheritDoc
11ade432
AE
192 */
193 public function getIndices($tableName) {
058cbd6a 194 $indices = [];
4f2c4eb8 195 $sql = "SHOW INDEX FROM `".$tableName."`";
11ade432
AE
196 $statement = $this->dbObj->prepareStatement($sql);
197 $statement->execute();
198 while ($row = $statement->fetchArray()) {
9f959ced
MS
199 $indices[] = $row['Key_name'];
200 }
201
f62c045d 202 return array_unique($indices);
11ade432
AE
203 }
204
205 /**
0fcfe5f6 206 * @inheritDoc
11ade432 207 */
058cbd6a 208 public function createTable($tableName, $columns, $indices = []) {
11ade432
AE
209 $columnDefinition = $indexDefinition = '';
210
211 // build column definition
212 foreach ($columns as $column) {
213 if (!empty($columnDefinition)) $columnDefinition .= ',';
214 $columnDefinition .= $this->buildColumnDefinition($column['name'], $column['data']);
215 }
216
217 // build index definition
218 $hasFulltextIndex = false;
219 foreach ($indices as $index) {
220 if (!empty($indexDefinition)) $indexDefinition .= ',';
221 $indexDefinition .= $this->buildIndexDefinition($index['name'], $index['data']);
222 if ($index['data']['type'] == 'FULLTEXT') $hasFulltextIndex = true;
223 }
224
225 // create table
4f2c4eb8 226 $sql = "CREATE TABLE `".$tableName."` (
11ade432
AE
227 ".$columnDefinition."
228 ".(!empty($indexDefinition) ? ',' : '')."
229 ".$indexDefinition."
ac677ff6 230 ) ENGINE=".($hasFulltextIndex ? 'MyISAM' : 'InnoDB')." DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
11ade432
AE
231 $statement = $this->dbObj->prepareStatement($sql);
232 $statement->execute();
233 }
234
235 /**
0fcfe5f6 236 * @inheritDoc
11ade432
AE
237 */
238 public function dropTable($tableName) {
4f2c4eb8 239 $sql = "DROP TABLE IF EXISTS `".$tableName."`";
11ade432
AE
240 $statement = $this->dbObj->prepareStatement($sql);
241 $statement->execute();
242 }
243
244 /**
0fcfe5f6 245 * @inheritDoc
11ade432
AE
246 */
247 public function addColumn($tableName, $columnName, $columnData) {
4f2c4eb8 248 $sql = "ALTER TABLE `".$tableName."` ADD COLUMN ".$this->buildColumnDefinition($columnName, $columnData);
11ade432
AE
249 $statement = $this->dbObj->prepareStatement($sql);
250 $statement->execute();
251 }
252
253 /**
0fcfe5f6 254 * @inheritDoc
11ade432
AE
255 */
256 public function alterColumn($tableName, $oldColumnName, $newColumnName, $newColumnData) {
4f2c4eb8 257 $sql = "ALTER TABLE `".$tableName."` CHANGE COLUMN `".$oldColumnName."` ".$this->buildColumnDefinition($newColumnName, $newColumnData);
11ade432
AE
258 $statement = $this->dbObj->prepareStatement($sql);
259 $statement->execute();
260 }
261
f6e43f2f
MS
262 /**
263 * @inheritDoc
264 */
265 public function alterColumns($tableName, $alterData) {
266 $queries = "";
267 foreach ($alterData as $columnName => $data) {
268 switch ($data['action']) {
269 case 'add':
270 $queries .= "ADD COLUMN {$this->buildColumnDefinition($columnName, $data['data'])},";
271 break;
272
273 case 'alter':
274 $queries .= "CHANGE COLUMN `{$columnName}` {$this->buildColumnDefinition($data['oldColumnName'], $data['data'])},";
275 break;
276
277 case 'drop':
278 $queries .= "DROP COLUMN `{$columnName}`,";
279 break;
280 }
281 }
282
283 $this->dbObj->prepareStatement("ALTER TABLE `{$tableName}` " . rtrim($queries, ','))->execute();
284 }
285
11ade432 286 /**
0fcfe5f6 287 * @inheritDoc
11ade432
AE
288 */
289 public function dropColumn($tableName, $columnName) {
bf64fe54
TD
290 try {
291 $sql = "ALTER TABLE `".$tableName."` DROP COLUMN `".$columnName."`";
292 $statement = $this->dbObj->prepareStatement($sql);
293 $statement->execute();
294 }
295 catch (DatabaseQueryExecutionException $e) {
296 if ($e->getCode() != '42000') {
297 throw $e;
298 }
299 if (in_array($columnName, array_column($this->getColumns($tableName), 'name'))) {
300 throw $e;
301 }
302 }
11ade432
AE
303 }
304
305 /**
0fcfe5f6 306 * @inheritDoc
11ade432
AE
307 */
308 public function addIndex($tableName, $indexName, $indexData) {
4f2c4eb8 309 $sql = "ALTER TABLE `".$tableName."` ADD ".$this->buildIndexDefinition($indexName, $indexData);
11ade432
AE
310 $statement = $this->dbObj->prepareStatement($sql);
311 $statement->execute();
312 }
313
314 /**
0fcfe5f6 315 * @inheritDoc
11ade432
AE
316 */
317 public function addForeignKey($tableName, $indexName, $indexData) {
4f2c4eb8 318 $sql = "ALTER TABLE `".$tableName."` ADD";
11ade432
AE
319
320 // add index name
96017679 321 if (!empty($indexName)) $sql .= " CONSTRAINT `".$indexName."`";
11ade432
AE
322
323 // add columns
4f2c4eb8 324 $sql .= " FOREIGN KEY (`".str_replace(',', '`,`', preg_replace('/\s+/', '', $indexData['columns']))."`)";
11ade432
AE
325
326 // add referenced table name
4f2c4eb8 327 $sql .= " REFERENCES `".$indexData['referencedTable']."`";
11ade432
AE
328
329 // add referenced columns
4f2c4eb8 330 $sql .= " (`".str_replace(',', '`,`', preg_replace('/\s+/', '', $indexData['referencedColumns']))."`)";
11ade432
AE
331
332 // add operation and action
333 if (!empty($indexData['operation'])) $sql .= " ON ".$indexData['operation']." ".$indexData['action'];
281ac362
TD
334 if (!empty($indexData['ON DELETE'])) $sql .= " ON DELETE ".$indexData['ON DELETE'];
335 if (!empty($indexData['ON UPDATE'])) $sql .= " ON UPDATE ".$indexData['ON UPDATE'];
11ade432
AE
336
337 $statement = $this->dbObj->prepareStatement($sql);
338 $statement->execute();
339 }
340
341 /**
0fcfe5f6 342 * @inheritDoc
11ade432
AE
343 */
344 public function dropIndex($tableName, $indexName) {
bf64fe54
TD
345 try {
346 $sql = "ALTER TABLE `".$tableName."` DROP INDEX `".$indexName."`";
347 $statement = $this->dbObj->prepareStatement($sql);
348 $statement->execute();
349 }
350 catch (DatabaseQueryExecutionException $e) {
351 if ($e->getCode() != '42000') {
352 throw $e;
353 }
354 if (in_array($indexName, $this->getIndices($tableName))) {
355 throw $e;
356 }
357 }
11ade432
AE
358 }
359
42a1e71f 360 /**
6b7d6653 361 * @inheritDoc
42a1e71f 362 */
363 public function dropPrimaryKey($tableName) {
bf64fe54
TD
364 try {
365 $sql = "ALTER TABLE ".$tableName." DROP PRIMARY KEY";
366 $statement = $this->dbObj->prepareStatement($sql);
367 $statement->execute();
368 }
369 catch (DatabaseQueryExecutionException $e) {
370 if ($e->getCode() != '42000') {
371 throw $e;
372 }
373 if (in_array("PRIMARY", $this->getIndices($tableName))) {
374 throw $e;
375 }
376 }
42a1e71f 377 }
378
b6ae7d04 379 /**
0fcfe5f6 380 * @inheritDoc
b6ae7d04
MW
381 */
382 public function dropForeignKey($tableName, $indexName) {
bf64fe54
TD
383 try {
384 $sql = "ALTER TABLE `".$tableName."` DROP FOREIGN KEY `".$indexName."`";
385 $statement = $this->dbObj->prepareStatement($sql);
386 $statement->execute();
387 }
388 catch (DatabaseQueryExecutionException $e) {
389 if ($e->getCode() != '42000') {
390 throw $e;
391 }
392 if (in_array($indexName, array_keys($this->getForeignKeys($tableName)))) {
393 throw $e;
394 }
395 }
b6ae7d04
MW
396 }
397
11ade432
AE
398 /**
399 * Builds a column definition for execution in a create table or alter table statement.
400 *
401 * @param string $columnName
402 * @param array $columnData
71952a87 403 * @return string
11ade432
AE
404 */
405 protected function buildColumnDefinition($columnName, $columnData) {
406 // column name
4f2c4eb8 407 $definition = "`".$columnName."`";
11ade432
AE
408 // column type
409 $definition .= " ".$columnData['type'];
f6e43f2f 410
11ade432
AE
411 // column length and decimals
412 if (!empty($columnData['length'])) {
413 $definition .= "(".$columnData['length'].(!empty($columnData['decimals']) ? ",".$columnData['decimals'] : "").")";
414 }
415 // enum / set
416 if ($columnData['type'] == 'enum' && !empty($columnData['values'])) {
417 $definition .= "(".$columnData['values'].")";
418 }
419 // not null / null
420 if (!empty($columnData['notNull'])) $definition .= " NOT NULL";
421 // default
422 if (isset($columnData['default']) && $columnData['default'] !== '') $definition .= " DEFAULT ".$columnData['default'];
423 // auto_increment
424 if (!empty($columnData['autoIncrement'])) $definition .= " AUTO_INCREMENT";
425 // key
426 if (!empty($columnData['key'])) $definition .= " ".$columnData['key']." KEY";
427
428 return $definition;
429 }
430
431 /**
432 * Builds a index definition for execution in a create table or alter table statement.
433 *
434 * @param string $indexName
435 * @param array $indexData
71952a87 436 * @return string
11ade432
AE
437 */
438 protected function buildIndexDefinition($indexName, $indexData) {
11ade432
AE
439 // index type
440 if ($indexData['type'] == 'PRIMARY') $definition = "PRIMARY KEY";
441 else if ($indexData['type'] == 'UNIQUE') $definition = "UNIQUE KEY";
442 else if ($indexData['type'] == 'FULLTEXT') $definition = "FULLTEXT KEY";
443 else $definition = "KEY";
444
445 // index name
4f2c4eb8 446 if (!empty($indexName)) $definition .= " `".$indexName."`";
11ade432 447 // columns
4f2c4eb8 448 $definition .= " (`".str_replace(',', '`,`', preg_replace('/\s+/', '', $indexData['columns']))."`)";
11ade432
AE
449
450 return $definition;
451 }
452}