1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Lagdo\DbAdmin\Driver\MySql\Db; |
4
|
|
|
|
5
|
|
|
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity; |
6
|
|
|
use Lagdo\DbAdmin\Driver\Entity\TableEntity; |
7
|
|
|
use Lagdo\DbAdmin\Driver\Entity\IndexEntity; |
8
|
|
|
use Lagdo\DbAdmin\Driver\Entity\ForeignKeyEntity; |
9
|
|
|
use Lagdo\DbAdmin\Driver\Entity\TriggerEntity; |
10
|
|
|
|
11
|
|
|
use Lagdo\DbAdmin\Driver\Db\ConnectionInterface; |
12
|
|
|
|
13
|
|
|
use Lagdo\DbAdmin\Driver\Db\Table as AbstractTable; |
14
|
|
|
|
15
|
|
|
class Table extends AbstractTable |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* @param bool $fast |
19
|
|
|
* @param string $table |
20
|
|
|
* |
21
|
|
|
* @return array |
22
|
|
|
*/ |
23
|
|
|
private function queryStatus(bool $fast, string $table = '') |
24
|
|
|
{ |
25
|
|
|
$query = ($fast && $this->driver->minVersion(5)) ? |
26
|
|
|
"SELECT TABLE_NAME AS Name, ENGINE AS Engine, TABLE_COMMENT AS Comment " . |
27
|
|
|
"FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() " . |
28
|
|
|
($table != "" ? "AND TABLE_NAME = " . $this->driver->quote($table) : "ORDER BY Name") : |
29
|
|
|
"SHOW TABLE STATUS" . ($table != "" ? " LIKE " . $this->driver->quote(addcslashes($table, "%_\\")) : ""); |
30
|
|
|
return $this->driver->rows($query); |
31
|
|
|
} |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @param array $row |
35
|
|
|
* |
36
|
|
|
* @return TableEntity |
37
|
|
|
*/ |
38
|
|
|
private function makeStatus(array $row) |
39
|
|
|
{ |
40
|
|
|
$status = new TableEntity($row['Name']); |
41
|
|
|
$status->engine = $row['Engine']; |
42
|
|
|
if ($row["Engine"] == "InnoDB") { |
43
|
|
|
// ignore internal comment, unnecessary since MySQL 5.1.21 |
44
|
|
|
$status->comment = preg_replace('~(?:(.+); )?InnoDB free: .*~', '\1', $row["Comment"]); |
45
|
|
|
} |
46
|
|
|
// if (!isset($row["Engine"])) { |
47
|
|
|
// $row["Comment"] = ""; |
48
|
|
|
// } |
49
|
|
|
|
50
|
|
|
return $status; |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @inheritDoc |
55
|
|
|
*/ |
56
|
|
|
public function tableStatus(string $table, bool $fast = false) |
57
|
|
|
{ |
58
|
|
|
$rows = $this->queryStatus($fast, $table); |
59
|
|
|
if (!($row = reset($rows))) { |
60
|
|
|
return null; |
61
|
|
|
} |
62
|
|
|
return $this->makeStatus($row); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @inheritDoc |
67
|
|
|
*/ |
68
|
|
|
public function tablesStatuses(bool $fast = false) |
69
|
|
|
{ |
70
|
|
|
$tables = []; |
71
|
|
|
$rows = $this->queryStatus($fast); |
72
|
|
|
foreach ($rows as $row) { |
73
|
|
|
$tables[$row["Name"]] = $this->makeStatus($row); |
74
|
|
|
} |
75
|
|
|
return $tables; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @inheritDoc |
80
|
|
|
*/ |
81
|
|
|
public function fields(string $table) |
82
|
|
|
{ |
83
|
|
|
$fields = []; |
84
|
|
|
foreach ($this->driver->rows("SHOW FULL COLUMNS FROM " . $this->driver->table($table)) as $row) { |
85
|
|
|
preg_match('~^([^( ]+)(?:\((.+)\))?( unsigned)?( zerofill)?$~', $row["Type"], $match); |
86
|
|
|
$matchCount = count($match); |
87
|
|
|
$match1 = $matchCount > 1 ? $match[1] : ''; |
88
|
|
|
$match2 = $matchCount > 2 ? $match[2] : ''; |
89
|
|
|
$match3 = $matchCount > 3 ? $match[3] : ''; |
90
|
|
|
$match4 = $matchCount > 4 ? $match[4] : ''; |
91
|
|
|
|
92
|
|
|
$field = new TableFieldEntity(); |
93
|
|
|
|
94
|
|
|
$field->name = $row["Field"]; |
95
|
|
|
$field->fullType = $row["Type"]; |
96
|
|
|
$field->type = $match1; |
97
|
|
|
$field->length = $match2; |
|
|
|
|
98
|
|
|
$field->unsigned = ltrim($match3 . $match4); |
99
|
|
|
$field->default = ($row["Default"] != "" || preg_match("~char|set~", $match1) ? |
100
|
|
|
(preg_match('~text~', $match1) ? stripslashes(preg_replace("~^'(.*)'\$~", '\1', |
101
|
|
|
$row["Default"])) : $row["Default"]) : null); |
102
|
|
|
$field->null = ($row["Null"] == "YES"); |
103
|
|
|
$field->autoIncrement = ($row["Extra"] == "auto_increment"); |
104
|
|
|
$field->onUpdate = (preg_match('~^on update (.+)~i', $row["Extra"], $match) ? $match1 : ""); //! available since MySQL 5.1.23 |
105
|
|
|
$field->collation = $row["Collation"]; |
106
|
|
|
$field->privileges = array_flip(preg_split('~, *~', $row["Privileges"])); |
107
|
|
|
$field->comment = $row["Comment"]; |
108
|
|
|
$field->primary = ($row["Key"] == "PRI"); |
109
|
|
|
// https://mariadb.com/kb/en/library/show-columns/ |
110
|
|
|
// https://github.com/vrana/adminer/pull/359#pullrequestreview-276677186 |
111
|
|
|
$field->generated = (preg_match('~^(VIRTUAL|PERSISTENT|STORED)~', $row["Extra"])); |
|
|
|
|
112
|
|
|
|
113
|
|
|
$fields[$field->name] = $field; |
114
|
|
|
} |
115
|
|
|
return $fields; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* @inheritDoc |
120
|
|
|
*/ |
121
|
|
|
public function supportForeignKeys(TableEntity $tableStatus) |
122
|
|
|
{ |
123
|
|
|
return preg_match('~InnoDB|IBMDB2I~i', $tableStatus->engine) |
124
|
|
|
|| (preg_match('~NDB~i', $tableStatus->engine) && $this->driver->minVersion(5.6)); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @inheritDoc |
129
|
|
|
*/ |
130
|
|
|
public function isView(TableEntity $tableStatus) |
131
|
|
|
{ |
132
|
|
|
return $tableStatus->engine === null; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* @inheritDoc |
137
|
|
|
*/ |
138
|
|
|
public function indexes(string $table, ConnectionInterface $connection = null) |
139
|
|
|
{ |
140
|
|
|
$indexes = []; |
141
|
|
|
foreach ($this->driver->rows("SHOW INDEX FROM " . $this->driver->table($table), $connection) as $row) { |
142
|
|
|
$index = new IndexEntity(); |
143
|
|
|
|
144
|
|
|
$name = $row["Key_name"]; |
145
|
|
|
$index->type = ($name == "PRIMARY" ? "PRIMARY" : |
146
|
|
|
($row["Index_type"] == "FULLTEXT" ? "FULLTEXT" : ($row["Non_unique"] ? |
147
|
|
|
($row["Index_type"] == "SPATIAL" ? "SPATIAL" : "INDEX") : "UNIQUE"))); |
148
|
|
|
$index->columns[] = $row["Column_name"]; |
149
|
|
|
$index->lengths[] = ($row["Index_type"] == "SPATIAL" ? null : $row["Sub_part"]); |
150
|
|
|
$index->descs[] = null; |
151
|
|
|
|
152
|
|
|
$indexes[$name] = $index; |
153
|
|
|
} |
154
|
|
|
return $indexes; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* @inheritDoc |
159
|
|
|
*/ |
160
|
|
|
public function foreignKeys(string $table) |
161
|
|
|
{ |
162
|
|
|
static $pattern = '(?:`(?:[^`]|``)+`|"(?:[^"]|"")+")'; |
163
|
|
|
$foreignKeys = []; |
164
|
|
|
$onActions = $this->driver->actions(); |
165
|
|
|
$create_table = $this->connection->result("SHOW CREATE TABLE " . $this->driver->table($table), 1); |
166
|
|
|
if ($create_table) { |
167
|
|
|
preg_match_all("~CONSTRAINT ($pattern) FOREIGN KEY ?\\(((?:$pattern,? ?)+)\\) REFERENCES " . |
168
|
|
|
"($pattern)(?:\\.($pattern))? \\(((?:$pattern,? ?)+)\\)(?: ON DELETE ($onActions))" . |
169
|
|
|
"?(?: ON UPDATE ($onActions))?~", $create_table, $matches, PREG_SET_ORDER); |
170
|
|
|
|
171
|
|
|
foreach ($matches as $match) { |
172
|
|
|
$matchCount = count($match); |
173
|
|
|
$match1 = $matchCount > 1 ? $match[1] : ''; |
174
|
|
|
$match2 = $matchCount > 2 ? $match[2] : ''; |
175
|
|
|
$match3 = $matchCount > 3 ? $match[3] : ''; |
176
|
|
|
$match4 = $matchCount > 4 ? $match[4] : ''; |
177
|
|
|
$match5 = $matchCount > 5 ? $match[5] : ''; |
178
|
|
|
|
179
|
|
|
preg_match_all("~$pattern~", $match2, $source); |
180
|
|
|
preg_match_all("~$pattern~", $match5, $target); |
181
|
|
|
|
182
|
|
|
$foreignKey = new ForeignKeyEntity(); |
183
|
|
|
|
184
|
|
|
$foreignKey->database = $this->driver->unescapeId($match4 != "" ? $match3 : $match4); |
185
|
|
|
$foreignKey->table = $this->driver->unescapeId($match4 != "" ? $match4 : $match3); |
186
|
|
|
$foreignKey->source = array_map(function ($idf) { |
187
|
|
|
return $this->driver->unescapeId($idf); |
188
|
|
|
}, $source[0]); |
189
|
|
|
$foreignKey->target = array_map(function ($idf) { |
190
|
|
|
return $this->driver->unescapeId($idf); |
191
|
|
|
}, $target[0]); |
192
|
|
|
$foreignKey->onDelete = $matchCount > 6 ? $match[6] : "RESTRICT"; |
193
|
|
|
$foreignKey->onUpdate = $matchCount > 7 ? $match[7] : "RESTRICT"; |
194
|
|
|
|
195
|
|
|
$foreignKeys[$this->driver->unescapeId($match1)] = $foreignKey; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
return $foreignKeys; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* @inheritDoc |
203
|
|
|
*/ |
204
|
|
|
public function alterTable(string $table, string $name, array $fields, array $foreign, |
205
|
|
|
string $comment, string $engine, string $collation, int $autoIncrement, string $partitioning) |
206
|
|
|
{ |
207
|
|
|
$alter = []; |
208
|
|
|
foreach ($fields as $field) { |
209
|
|
|
$alter[] = ($field[1] ? ($table != "" ? ($field[0] != "" ? "CHANGE " . |
210
|
|
|
$this->driver->escapeId($field[0]) : "ADD") : " ") . " " . |
211
|
|
|
implode($field[1]) . ($table != "" ? $field[2] : "") : |
212
|
|
|
"DROP " . $this->driver->escapeId($field[0]) |
213
|
|
|
); |
214
|
|
|
} |
215
|
|
|
$alter = array_merge($alter, $foreign); |
216
|
|
|
$status = " COMMENT=" . $this->driver->quote($comment) . |
217
|
|
|
($engine ? " ENGINE=" . $this->driver->quote($engine) : "") . |
218
|
|
|
($collation ? " COLLATE " . $this->driver->quote($collation) : "") . |
219
|
|
|
($autoIncrement != "" ? " AUTO_INCREMENT=$autoIncrement" : ""); |
220
|
|
|
if ($table == "") { |
221
|
|
|
return $this->driver->execute("CREATE TABLE " . $this->driver->table($name) . |
|
|
|
|
222
|
|
|
" (\n" . implode(",\n", $alter) . "\n)$status$partitioning"); |
223
|
|
|
} |
224
|
|
|
if ($table != $name) { |
225
|
|
|
$alter[] = "RENAME TO " . $this->driver->table($name); |
226
|
|
|
} |
227
|
|
|
if ($status) { |
228
|
|
|
$alter[] = ltrim($status); |
229
|
|
|
} |
230
|
|
|
return ($alter || $partitioning ? $this->driver->execute("ALTER TABLE " . |
|
|
|
|
231
|
|
|
$this->driver->table($table) . "\n" . implode(",\n", $alter) . $partitioning) : true); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* @inheritDoc |
236
|
|
|
*/ |
237
|
|
|
public function alterIndexes(string $table, array $alter) |
238
|
|
|
{ |
239
|
|
|
foreach ($alter as $key => $val) { |
240
|
|
|
$alter[$key] = ( |
241
|
|
|
$val[2] == "DROP" |
242
|
|
|
? "\nDROP INDEX " . $this->driver->escapeId($val[1]) |
243
|
|
|
: "\nADD $val[0] " . ($val[0] == "PRIMARY" ? "KEY " : "") . ($val[1] != "" ? $this->driver->escapeId($val[1]) . " " : "") . "(" . implode(", ", $val[2]) . ")" |
244
|
|
|
); |
245
|
|
|
} |
246
|
|
|
return $this->driver->execute("ALTER TABLE " . $this->driver->table($table) . implode(",", $alter)); |
|
|
|
|
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* @inheritDoc |
251
|
|
|
*/ |
252
|
|
|
public function trigger(string $trigger) |
253
|
|
|
{ |
254
|
|
|
if ($trigger == "") { |
255
|
|
|
return null; |
256
|
|
|
} |
257
|
|
|
$rows = $this->driver->rows("SHOW TRIGGERS WHERE `Trigger` = " . $this->driver->quote($trigger)); |
258
|
|
|
if (!($row = reset($rows))) { |
259
|
|
|
return null; |
260
|
|
|
} |
261
|
|
|
return new TriggerEntity($row["Timing"], $row["Event"]); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @inheritDoc |
266
|
|
|
*/ |
267
|
|
|
public function triggers(string $table) |
268
|
|
|
{ |
269
|
|
|
$triggers = []; |
270
|
|
|
foreach ($this->driver->rows("SHOW TRIGGERS LIKE " . $this->driver->quote(addcslashes($table, "%_\\"))) as $row) { |
271
|
|
|
$triggers[$row["Trigger"]] = new TriggerEntity($row["Timing"], $row["Event"]); |
272
|
|
|
} |
273
|
|
|
return $triggers; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* @inheritDoc |
278
|
|
|
*/ |
279
|
|
|
public function triggerOptions() |
280
|
|
|
{ |
281
|
|
|
return [ |
282
|
|
|
"Timing" => ["BEFORE", "AFTER"], |
283
|
|
|
"Event" => ["INSERT", "UPDATE", "DELETE"], |
284
|
|
|
"Type" => ["FOR EACH ROW"], |
285
|
|
|
]; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
/** |
289
|
|
|
* @inheritDoc |
290
|
|
|
*/ |
291
|
|
|
public function tableHelp(string $name) |
292
|
|
|
{ |
293
|
|
|
$maria = preg_match('~MariaDB~', $this->connection->serverInfo()); |
294
|
|
|
if ($this->driver->isInformationSchema($this->driver->database())) { |
295
|
|
|
return strtolower(($maria ? "information-schema-$name-table/" : str_replace("_", "-", $name) . "-table.html")); |
296
|
|
|
} |
297
|
|
|
if ($this->driver->database() == "mysql") { |
298
|
|
|
return ($maria ? "mysql$name-table/" : "system-database.html"); //! more precise link |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
} |
302
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.