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