1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace PhpMyAdmin\Controllers\Database; |
6
|
|
|
|
7
|
|
|
use PhpMyAdmin\Charsets; |
8
|
|
|
use PhpMyAdmin\Config; |
9
|
|
|
use PhpMyAdmin\Config\PageSettings; |
10
|
|
|
use PhpMyAdmin\ConfigStorage\Relation; |
11
|
|
|
use PhpMyAdmin\Controllers\InvocableController; |
12
|
|
|
use PhpMyAdmin\Current; |
13
|
|
|
use PhpMyAdmin\DatabaseInterface; |
14
|
|
|
use PhpMyAdmin\DbTableExists; |
15
|
|
|
use PhpMyAdmin\Favorites\RecentFavoriteTable; |
16
|
|
|
use PhpMyAdmin\Favorites\RecentFavoriteTables; |
17
|
|
|
use PhpMyAdmin\Favorites\TableType; |
18
|
|
|
use PhpMyAdmin\Html\Generator; |
19
|
|
|
use PhpMyAdmin\Http\Response; |
20
|
|
|
use PhpMyAdmin\Http\ServerRequest; |
21
|
|
|
use PhpMyAdmin\Identifiers\DatabaseName; |
|
|
|
|
22
|
|
|
use PhpMyAdmin\Identifiers\TableName; |
|
|
|
|
23
|
|
|
use PhpMyAdmin\LanguageManager; |
24
|
|
|
use PhpMyAdmin\Message; |
25
|
|
|
use PhpMyAdmin\Query\Utilities; |
26
|
|
|
use PhpMyAdmin\Replication\Replication; |
27
|
|
|
use PhpMyAdmin\Replication\ReplicationInfo; |
28
|
|
|
use PhpMyAdmin\ResponseRenderer; |
29
|
|
|
use PhpMyAdmin\Sanitize; |
30
|
|
|
use PhpMyAdmin\StorageEngine; |
31
|
|
|
use PhpMyAdmin\Template; |
32
|
|
|
use PhpMyAdmin\Tracking\TrackedTable; |
33
|
|
|
use PhpMyAdmin\Tracking\Tracker; |
34
|
|
|
use PhpMyAdmin\Tracking\TrackingChecker; |
35
|
|
|
use PhpMyAdmin\Url; |
36
|
|
|
use PhpMyAdmin\Util; |
37
|
|
|
|
38
|
|
|
use function __; |
39
|
|
|
use function array_search; |
40
|
|
|
use function ceil; |
41
|
|
|
use function count; |
42
|
|
|
use function htmlspecialchars; |
43
|
|
|
use function implode; |
44
|
|
|
use function in_array; |
45
|
|
|
use function is_string; |
46
|
|
|
use function max; |
47
|
|
|
use function mb_substr; |
48
|
|
|
use function md5; |
49
|
|
|
use function preg_match; |
50
|
|
|
use function preg_quote; |
51
|
|
|
use function sprintf; |
52
|
|
|
use function str_replace; |
53
|
|
|
use function strtotime; |
54
|
|
|
use function urlencode; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Handles database structure logic |
58
|
|
|
*/ |
59
|
|
|
final class StructureController implements InvocableController |
60
|
|
|
{ |
61
|
|
|
/** @var int Number of tables */ |
62
|
|
|
private int $numTables = 0; |
63
|
|
|
|
64
|
|
|
/** @var int Current position in the list */ |
65
|
|
|
private int $position = 0; |
66
|
|
|
|
67
|
|
|
/** @var bool DB is information_schema */ |
68
|
|
|
private bool $dbIsSystemSchema = false; |
69
|
|
|
|
70
|
|
|
/** @var int Number of tables */ |
71
|
|
|
private int $totalNumTables = 0; |
72
|
|
|
|
73
|
|
|
/** @var mixed[] Tables in the database */ |
74
|
|
|
private array $tables = []; |
75
|
|
|
|
76
|
|
|
/** @var bool whether stats show or not */ |
77
|
|
|
private bool $isShowStats = false; |
78
|
|
|
|
79
|
|
|
private ReplicationInfo $replicationInfo; |
80
|
|
|
|
81
|
20 |
|
public function __construct( |
82
|
|
|
private readonly ResponseRenderer $response, |
83
|
|
|
private readonly Template $template, |
84
|
|
|
private readonly Relation $relation, |
85
|
|
|
private readonly Replication $replication, |
86
|
|
|
private readonly DatabaseInterface $dbi, |
87
|
|
|
private readonly TrackingChecker $trackingChecker, |
88
|
|
|
private readonly PageSettings $pageSettings, |
89
|
|
|
private readonly DbTableExists $dbTableExists, |
90
|
|
|
) { |
91
|
20 |
|
$this->replicationInfo = new ReplicationInfo($this->dbi); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Retrieves database information for further use. |
96
|
|
|
*/ |
97
|
4 |
|
private function getDatabaseInfo(ServerRequest $request): void |
98
|
|
|
{ |
99
|
4 |
|
[$tables, $totalNumTables] = Util::getDbInfo($request, Current::$database); |
100
|
|
|
|
101
|
4 |
|
$this->tables = $tables; |
102
|
4 |
|
$this->numTables = count($tables); |
103
|
4 |
|
$this->position = Util::getTableListPosition($request, Current::$database); |
104
|
4 |
|
$this->totalNumTables = $totalNumTables; |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* whether to display extended stats |
108
|
|
|
*/ |
109
|
4 |
|
$this->isShowStats = Config::getInstance()->settings['ShowStats']; |
|
|
|
|
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* whether selected db is information_schema |
113
|
|
|
*/ |
114
|
4 |
|
$this->dbIsSystemSchema = false; |
115
|
|
|
|
116
|
4 |
|
if (! Utilities::isSystemSchema(Current::$database)) { |
117
|
4 |
|
return; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$this->isShowStats = false; |
121
|
|
|
$this->dbIsSystemSchema = true; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
public function __invoke(ServerRequest $request): Response|null |
125
|
|
|
{ |
126
|
|
|
$GLOBALS['errorUrl'] ??= null; |
127
|
|
|
|
128
|
|
|
$parameters = ['sort' => $_REQUEST['sort'] ?? null, 'sort_order' => $_REQUEST['sort_order'] ?? null]; |
129
|
|
|
|
130
|
|
|
if (! $this->response->checkParameters(['db'])) { |
131
|
|
|
return null; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
$config = Config::getInstance(); |
|
|
|
|
135
|
|
|
$GLOBALS['errorUrl'] = Util::getScriptNameForOption($config->settings['DefaultTabDatabase'], 'database'); |
136
|
|
|
$GLOBALS['errorUrl'] .= Url::getCommon(['db' => Current::$database], '&'); |
137
|
|
|
|
138
|
|
|
$databaseName = DatabaseName::tryFrom($request->getParam('db')); |
139
|
|
|
if ($databaseName === null || ! $this->dbTableExists->selectDatabase($databaseName)) { |
140
|
|
|
if ($request->isAjax()) { |
141
|
|
|
$this->response->setRequestStatus(false); |
142
|
|
|
$this->response->addJSON('message', Message::error(__('No databases selected.'))); |
143
|
|
|
|
144
|
|
|
return null; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
$this->response->redirectToRoute('/', ['reload' => true, 'message' => __('No databases selected.')]); |
148
|
|
|
|
149
|
|
|
return null; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
$this->response->addScriptFiles(['database/structure.js', 'table/change.js']); |
153
|
|
|
|
154
|
|
|
// Gets the database structure |
155
|
|
|
$this->getDatabaseInfo($request); |
156
|
|
|
|
157
|
|
|
// Checks if there are any tables to be shown on current page. |
158
|
|
|
// If there are no tables, the user is redirected to the last page |
159
|
|
|
// having any. |
160
|
|
|
if ($this->totalNumTables > 0 && $this->position > $this->totalNumTables) { |
161
|
|
|
$this->response->redirectToRoute('/database/structure', [ |
162
|
|
|
'db' => Current::$database, |
163
|
|
|
'pos' => max(0, $this->totalNumTables - $config->settings['MaxTableList']), |
164
|
|
|
'reload' => 1, |
165
|
|
|
]); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
$this->replicationInfo->load($request->getParsedBodyParam('primary_connection')); |
169
|
|
|
$replicaInfo = $this->replicationInfo->getReplicaInfo(); |
170
|
|
|
|
171
|
|
|
$this->pageSettings->init('DbStructure'); |
172
|
|
|
$this->response->addHTML($this->pageSettings->getErrorHTML()); |
173
|
|
|
$this->response->addHTML($this->pageSettings->getHTML()); |
174
|
|
|
|
175
|
|
|
if ($this->numTables > 0) { |
176
|
|
|
$urlParams = ['pos' => $this->position, 'db' => Current::$database]; |
177
|
|
|
if (isset($parameters['sort'])) { |
178
|
|
|
$urlParams['sort'] = $parameters['sort']; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
if (isset($parameters['sort_order'])) { |
182
|
|
|
$urlParams['sort_order'] = $parameters['sort_order']; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
$listNavigator = Generator::getListNavigator( |
186
|
|
|
$this->totalNumTables, |
187
|
|
|
$this->position, |
188
|
|
|
$urlParams, |
189
|
|
|
Url::getFromRoute('/database/structure'), |
190
|
|
|
'frame_content', |
191
|
|
|
$config->settings['MaxTableList'], |
192
|
|
|
); |
193
|
|
|
|
194
|
|
|
$tableList = $this->displayTableList($replicaInfo); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
$createTable = ''; |
198
|
|
|
if (! $this->dbIsSystemSchema) { |
199
|
|
|
$createTable = $this->template->render('database/create_table', ['db' => Current::$database]); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
$this->response->render('database/structure/index', [ |
203
|
|
|
'database' => Current::$database, |
204
|
|
|
'has_tables' => $this->numTables > 0, |
205
|
|
|
'list_navigator_html' => $listNavigator ?? '', |
206
|
|
|
'table_list_html' => $tableList ?? '', |
207
|
|
|
'is_system_schema' => $this->dbIsSystemSchema, |
208
|
|
|
'create_table_html' => $createTable, |
209
|
|
|
]); |
210
|
|
|
|
211
|
|
|
return null; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** @param mixed[] $replicaInfo */ |
215
|
4 |
|
private function displayTableList(array $replicaInfo): string |
216
|
|
|
{ |
217
|
4 |
|
$html = ''; |
218
|
|
|
|
219
|
|
|
// filtering |
220
|
4 |
|
$html .= $this->template->render('filter', ['filter_value' => '']); |
221
|
|
|
|
222
|
4 |
|
$i = $sumEntries = 0; |
223
|
4 |
|
$overheadCheck = false; |
224
|
4 |
|
$createTimeAll = ''; |
225
|
4 |
|
$updateTimeAll = ''; |
226
|
4 |
|
$checkTimeAll = ''; |
227
|
4 |
|
$config = Config::getInstance(); |
|
|
|
|
228
|
4 |
|
$numColumns = $config->settings['PropertiesNumColumns'] > 1 |
229
|
|
|
? ceil($this->numTables / $config->settings['PropertiesNumColumns']) + 1 |
230
|
4 |
|
: 0; |
231
|
4 |
|
$rowCount = 0; |
232
|
4 |
|
$sumSize = 0; |
233
|
4 |
|
$overheadSize = 0; |
234
|
|
|
|
235
|
4 |
|
$hiddenFields = []; |
236
|
4 |
|
$overallApproxRows = false; |
237
|
4 |
|
$structureTableRows = []; |
238
|
4 |
|
$trackedTables = $this->trackingChecker->getTrackedTables(Current::$database); |
239
|
4 |
|
$recentFavoriteTables = RecentFavoriteTables::getInstance(TableType::Favorite); |
240
|
4 |
|
foreach ($this->tables as $currentTable) { |
241
|
|
|
// Get valid statistics whatever is the table type |
242
|
|
|
|
243
|
4 |
|
$dropQuery = ''; |
244
|
4 |
|
$dropMessage = ''; |
245
|
4 |
|
$overhead = ''; |
246
|
4 |
|
$inputClass = ['checkall']; |
247
|
|
|
|
248
|
|
|
// Sets parameters for links |
249
|
4 |
|
$tableUrlParams = ['db' => Current::$database, 'table' => $currentTable['TABLE_NAME']]; |
250
|
|
|
// do not list the previous table's size info for a view |
251
|
|
|
|
252
|
4 |
|
[ |
253
|
4 |
|
$currentTable, |
254
|
4 |
|
$formattedSize, |
255
|
4 |
|
$unit, |
256
|
4 |
|
$formattedOverhead, |
257
|
4 |
|
$overheadUnit, |
258
|
4 |
|
$overheadSize, |
259
|
4 |
|
$tableIsView, |
260
|
4 |
|
$sumSize, |
261
|
4 |
|
] = $this->getStuffForEngineTypeTable($currentTable, $sumSize, $overheadSize); |
262
|
|
|
|
263
|
4 |
|
$curTable = $this->dbi |
264
|
4 |
|
->getTable(Current::$database, $currentTable['TABLE_NAME']); |
265
|
4 |
|
if (! $curTable->isMerge()) { |
266
|
4 |
|
$sumEntries += $currentTable['TABLE_ROWS']; |
267
|
|
|
} |
268
|
|
|
|
269
|
4 |
|
$collationDefinition = '---'; |
270
|
4 |
|
if (isset($currentTable['Collation'])) { |
271
|
|
|
$tableCollation = Charsets::findCollationByName( |
272
|
|
|
$this->dbi, |
273
|
|
|
$config->selectedServer['DisableIS'], |
274
|
|
|
$currentTable['Collation'], |
275
|
|
|
); |
276
|
|
|
if ($tableCollation !== null) { |
277
|
|
|
$collationDefinition = $this->template->render('database/structure/collation_definition', [ |
278
|
|
|
'valueTitle' => $tableCollation->getDescription(), |
279
|
|
|
'value' => $tableCollation->getName(), |
280
|
|
|
]); |
281
|
|
|
} |
282
|
|
|
} |
283
|
|
|
|
284
|
4 |
|
if ($this->isShowStats) { |
285
|
4 |
|
$overhead = '-'; |
286
|
4 |
|
if ($formattedOverhead != '') { |
287
|
4 |
|
$overhead = $this->template->render('database/structure/overhead', [ |
288
|
4 |
|
'table_url_params' => $tableUrlParams, |
289
|
4 |
|
'formatted_overhead' => $formattedOverhead, |
290
|
4 |
|
'overhead_unit' => $overheadUnit, |
291
|
4 |
|
]); |
292
|
4 |
|
$overheadCheck = true; |
293
|
4 |
|
$inputClass[] = 'tbl-overhead'; |
294
|
|
|
} |
295
|
|
|
} |
296
|
|
|
|
297
|
4 |
|
if ($config->settings['ShowDbStructureCharset']) { |
298
|
|
|
$charset = ''; |
299
|
|
|
if (isset($tableCollation)) { |
300
|
|
|
$charset = $tableCollation->getCharset(); |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
4 |
|
if ($config->settings['ShowDbStructureCreation']) { |
305
|
|
|
$createTime = $currentTable['Create_time'] ?? ''; |
306
|
|
|
if ($createTime && (! $createTimeAll || $createTime < $createTimeAll)) { |
307
|
|
|
$createTimeAll = $createTime; |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
311
|
4 |
|
if ($config->settings['ShowDbStructureLastUpdate']) { |
312
|
|
|
$updateTime = $currentTable['Update_time'] ?? ''; |
313
|
|
|
if ($updateTime && (! $updateTimeAll || $updateTime < $updateTimeAll)) { |
314
|
|
|
$updateTimeAll = $updateTime; |
315
|
|
|
} |
316
|
|
|
} |
317
|
|
|
|
318
|
4 |
|
if ($config->settings['ShowDbStructureLastCheck']) { |
319
|
|
|
$checkTime = $currentTable['Check_time'] ?? ''; |
320
|
|
|
if ($checkTime && (! $checkTimeAll || $checkTime < $checkTimeAll)) { |
321
|
|
|
$checkTimeAll = $checkTime; |
322
|
|
|
} |
323
|
|
|
} |
324
|
|
|
|
325
|
4 |
|
$truename = $currentTable['TABLE_NAME']; |
326
|
|
|
|
327
|
4 |
|
$i++; |
328
|
|
|
|
329
|
4 |
|
$rowCount++; |
330
|
4 |
|
if ($tableIsView) { |
331
|
|
|
$hiddenFields[] = '<input type="hidden" name="views[]" value="' |
332
|
|
|
. htmlspecialchars($currentTable['TABLE_NAME']) . '">'; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Always activate links for Browse, Search and Empty, even if |
337
|
|
|
* the icons are greyed, because |
338
|
|
|
* 1. for views, we don't know the number of rows at this point |
339
|
|
|
* 2. for tables, another source could have populated them since the |
340
|
|
|
* page was generated |
341
|
|
|
* |
342
|
|
|
* I could have used the PHP ternary conditional operator but I find |
343
|
|
|
* the code easier to read without this operator. |
344
|
|
|
*/ |
345
|
4 |
|
$mayHaveRows = $currentTable['TABLE_ROWS'] > 0 || $tableIsView; |
|
|
|
|
346
|
|
|
|
347
|
4 |
|
if (! $this->dbIsSystemSchema) { |
348
|
4 |
|
$dropQuery = sprintf( |
349
|
4 |
|
'DROP %s %s', |
350
|
4 |
|
$tableIsView || $currentTable['ENGINE'] == null ? 'VIEW' |
351
|
4 |
|
: 'TABLE', |
352
|
4 |
|
Util::backquote( |
353
|
4 |
|
$currentTable['TABLE_NAME'], |
354
|
4 |
|
), |
355
|
4 |
|
); |
356
|
4 |
|
$dropMessage = sprintf( |
357
|
4 |
|
($tableIsView || $currentTable['ENGINE'] == null |
358
|
|
|
? __('View %s has been dropped.') |
359
|
4 |
|
: __('Table %s has been dropped.')), |
360
|
4 |
|
str_replace( |
361
|
4 |
|
' ', |
362
|
4 |
|
' ', |
363
|
4 |
|
htmlspecialchars($currentTable['TABLE_NAME']), |
364
|
4 |
|
), |
365
|
4 |
|
); |
366
|
|
|
} |
367
|
|
|
|
368
|
4 |
|
if ($numColumns > 0 && $this->numTables > $numColumns && ($rowCount % $numColumns) === 0) { |
369
|
|
|
$rowCount = 1; |
370
|
|
|
|
371
|
|
|
$html .= $this->template->render('database/structure/table_header', [ |
372
|
|
|
'db' => Current::$database, |
373
|
|
|
'db_is_system_schema' => $this->dbIsSystemSchema, |
374
|
|
|
'replication' => $replicaInfo['status'], |
375
|
|
|
'properties_num_columns' => $config->settings['PropertiesNumColumns'], |
376
|
|
|
'is_show_stats' => $this->isShowStats, |
377
|
|
|
'show_charset' => $config->settings['ShowDbStructureCharset'], |
378
|
|
|
'show_comment' => $config->settings['ShowDbStructureComment'], |
379
|
|
|
'show_creation' => $config->settings['ShowDbStructureCreation'], |
380
|
|
|
'show_last_update' => $config->settings['ShowDbStructureLastUpdate'], |
381
|
|
|
'show_last_check' => $config->settings['ShowDbStructureLastCheck'], |
382
|
|
|
'num_favorite_tables' => $config->settings['NumFavoriteTables'], |
383
|
|
|
'structure_table_rows' => $structureTableRows, |
384
|
|
|
]); |
385
|
|
|
$structureTableRows = []; |
386
|
|
|
} |
387
|
|
|
|
388
|
4 |
|
[$approxRows, $showSuperscript] = $this->isRowCountApproximated($currentTable, $tableIsView); |
389
|
|
|
|
390
|
4 |
|
[$do, $ignored] = $this->getReplicationStatus($replicaInfo, $truename); |
391
|
|
|
|
392
|
4 |
|
$structureTableRows[] = [ |
393
|
4 |
|
'table_name_hash' => md5($currentTable['TABLE_NAME']), |
394
|
4 |
|
'db_table_name_hash' => md5(Current::$database . '.' . $currentTable['TABLE_NAME']), |
395
|
4 |
|
'db' => Current::$database, |
396
|
4 |
|
'curr' => $i, |
397
|
4 |
|
'input_class' => implode(' ', $inputClass), |
398
|
4 |
|
'table_is_view' => $tableIsView, |
399
|
4 |
|
'current_table' => $currentTable, |
400
|
4 |
|
'may_have_rows' => $mayHaveRows, |
401
|
4 |
|
'browse_table_label_title' => htmlspecialchars($currentTable['TABLE_COMMENT']), |
402
|
4 |
|
'browse_table_label_truename' => $truename, |
403
|
4 |
|
'empty_table_sql_query' => 'TRUNCATE ' . Util::backquote($currentTable['TABLE_NAME']), |
404
|
4 |
|
'empty_table_message_to_show' => urlencode( |
405
|
4 |
|
sprintf( |
406
|
4 |
|
__('Table %s has been emptied.'), |
407
|
4 |
|
htmlspecialchars( |
408
|
4 |
|
$currentTable['TABLE_NAME'], |
409
|
4 |
|
), |
410
|
4 |
|
), |
411
|
4 |
|
), |
412
|
4 |
|
'tracking_icon' => $this->getTrackingIcon($truename, $trackedTables[$truename] ?? null), |
413
|
4 |
|
'server_replica_status' => $replicaInfo['status'], |
414
|
4 |
|
'table_url_params' => $tableUrlParams, |
415
|
4 |
|
'db_is_system_schema' => $this->dbIsSystemSchema, |
416
|
4 |
|
'drop_query' => $dropQuery, |
417
|
4 |
|
'drop_message' => $dropMessage, |
418
|
4 |
|
'collation' => $collationDefinition, |
419
|
4 |
|
'formatted_size' => $formattedSize, |
420
|
4 |
|
'unit' => $unit, |
421
|
4 |
|
'overhead' => $overhead, |
422
|
4 |
|
'create_time' => isset($createTime) && $createTime |
423
|
4 |
|
? Util::localisedDate(strtotime($createTime)) : '-', |
424
|
4 |
|
'update_time' => isset($updateTime) && $updateTime |
425
|
4 |
|
? Util::localisedDate(strtotime($updateTime)) : '-', |
426
|
4 |
|
'check_time' => isset($checkTime) && $checkTime |
427
|
4 |
|
? Util::localisedDate(strtotime($checkTime)) : '-', |
428
|
4 |
|
'charset' => $charset ?? '', |
429
|
4 |
|
'is_show_stats' => $this->isShowStats, |
430
|
4 |
|
'ignored' => $ignored, |
431
|
4 |
|
'do' => $do, |
432
|
4 |
|
'approx_rows' => $approxRows, |
433
|
4 |
|
'show_superscript' => $showSuperscript, |
434
|
4 |
|
'already_favorite' => $recentFavoriteTables->contains( |
435
|
4 |
|
new RecentFavoriteTable( |
436
|
4 |
|
DatabaseName::from(Current::$database), |
437
|
4 |
|
TableName::from($currentTable['TABLE_NAME']), |
438
|
4 |
|
), |
439
|
4 |
|
), |
440
|
4 |
|
'num_favorite_tables' => $config->settings['NumFavoriteTables'], |
441
|
4 |
|
'properties_num_columns' => $config->settings['PropertiesNumColumns'], |
442
|
4 |
|
'limit_chars' => $config->settings['LimitChars'], |
443
|
4 |
|
'show_charset' => $config->settings['ShowDbStructureCharset'], |
444
|
4 |
|
'show_comment' => $config->settings['ShowDbStructureComment'], |
445
|
4 |
|
'show_creation' => $config->settings['ShowDbStructureCreation'], |
446
|
4 |
|
'show_last_update' => $config->settings['ShowDbStructureLastUpdate'], |
447
|
4 |
|
'show_last_check' => $config->settings['ShowDbStructureLastCheck'], |
448
|
4 |
|
]; |
449
|
|
|
|
450
|
4 |
|
$overallApproxRows = $overallApproxRows || $approxRows; |
451
|
|
|
} |
452
|
|
|
|
453
|
4 |
|
$databaseCollation = []; |
454
|
4 |
|
$databaseCharset = ''; |
455
|
4 |
|
$collation = Charsets::findCollationByName( |
456
|
4 |
|
$this->dbi, |
457
|
4 |
|
$config->selectedServer['DisableIS'], |
458
|
4 |
|
$this->dbi->getDbCollation(Current::$database), |
459
|
4 |
|
); |
460
|
4 |
|
if ($collation !== null) { |
461
|
|
|
$databaseCollation = ['name' => $collation->getName(), 'description' => $collation->getDescription()]; |
462
|
|
|
$databaseCharset = $collation->getCharset(); |
463
|
|
|
} |
464
|
|
|
|
465
|
4 |
|
$relationParameters = $this->relation->getRelationParameters(); |
466
|
|
|
|
467
|
4 |
|
$defaultStorageEngine = ''; |
468
|
4 |
|
if ($config->settings['PropertiesNumColumns'] < 2) { |
469
|
|
|
// MySQL <= 5.5.2 |
470
|
4 |
|
$defaultStorageEngine = $this->dbi->fetchValue('SELECT @@storage_engine;'); |
471
|
4 |
|
if (! is_string($defaultStorageEngine) || $defaultStorageEngine === '') { |
472
|
|
|
// MySQL >= 5.5.3 |
473
|
4 |
|
$defaultStorageEngine = $this->dbi->fetchValue('SELECT @@default_storage_engine;'); |
474
|
|
|
} |
475
|
|
|
} |
476
|
|
|
|
477
|
4 |
|
return $html . $this->template->render('database/structure/table_header', [ |
478
|
4 |
|
'db' => Current::$database, |
479
|
4 |
|
'db_is_system_schema' => $this->dbIsSystemSchema, |
480
|
4 |
|
'replication' => $replicaInfo['status'], |
481
|
4 |
|
'properties_num_columns' => $config->settings['PropertiesNumColumns'], |
482
|
4 |
|
'is_show_stats' => $this->isShowStats, |
483
|
4 |
|
'show_charset' => $config->settings['ShowDbStructureCharset'], |
484
|
4 |
|
'show_comment' => $config->settings['ShowDbStructureComment'], |
485
|
4 |
|
'show_creation' => $config->settings['ShowDbStructureCreation'], |
486
|
4 |
|
'show_last_update' => $config->settings['ShowDbStructureLastUpdate'], |
487
|
4 |
|
'show_last_check' => $config->settings['ShowDbStructureLastCheck'], |
488
|
4 |
|
'num_favorite_tables' => $config->settings['NumFavoriteTables'], |
489
|
4 |
|
'structure_table_rows' => $structureTableRows, |
490
|
4 |
|
'body_for_table_summary' => [ |
491
|
4 |
|
'num_tables' => $this->numTables, |
492
|
4 |
|
'server_replica_status' => $replicaInfo['status'], |
493
|
4 |
|
'db_is_system_schema' => $this->dbIsSystemSchema, |
494
|
4 |
|
'sum_entries' => $sumEntries, |
495
|
4 |
|
'database_collation' => $databaseCollation, |
496
|
4 |
|
'is_show_stats' => $this->isShowStats, |
497
|
4 |
|
'database_charset' => $databaseCharset, |
498
|
4 |
|
'sum_size' => $sumSize, |
499
|
4 |
|
'overhead_size' => $overheadSize, |
500
|
4 |
|
'create_time_all' => $createTimeAll ? Util::localisedDate(strtotime($createTimeAll)) : '-', |
501
|
4 |
|
'update_time_all' => $updateTimeAll ? Util::localisedDate(strtotime($updateTimeAll)) : '-', |
502
|
4 |
|
'check_time_all' => $checkTimeAll ? Util::localisedDate(strtotime($checkTimeAll)) : '-', |
503
|
4 |
|
'approx_rows' => $overallApproxRows, |
504
|
4 |
|
'num_favorite_tables' => $config->settings['NumFavoriteTables'], |
505
|
4 |
|
'db' => Current::$database, |
506
|
4 |
|
'properties_num_columns' => $config->settings['PropertiesNumColumns'], |
507
|
4 |
|
'default_storage_engine' => $defaultStorageEngine, |
508
|
4 |
|
'show_charset' => $config->settings['ShowDbStructureCharset'], |
509
|
4 |
|
'show_comment' => $config->settings['ShowDbStructureComment'], |
510
|
4 |
|
'show_creation' => $config->settings['ShowDbStructureCreation'], |
511
|
4 |
|
'show_last_update' => $config->settings['ShowDbStructureLastUpdate'], |
512
|
4 |
|
'show_last_check' => $config->settings['ShowDbStructureLastCheck'], |
513
|
4 |
|
], |
514
|
4 |
|
'check_all_tables' => [ |
515
|
4 |
|
'text_dir' => LanguageManager::$textDir, |
516
|
4 |
|
'overhead_check' => $overheadCheck, |
517
|
4 |
|
'db_is_system_schema' => $this->dbIsSystemSchema, |
518
|
4 |
|
'hidden_fields' => $hiddenFields, |
519
|
4 |
|
'disable_multi_table' => $config->settings['DisableMultiTableMaintenance'], |
520
|
4 |
|
'central_columns_work' => $relationParameters->centralColumnsFeature !== null, |
521
|
4 |
|
], |
522
|
4 |
|
]); |
523
|
|
|
} |
524
|
|
|
|
525
|
|
|
/** |
526
|
|
|
* Returns the tracking icon if the table is tracked |
527
|
|
|
* |
528
|
|
|
* @return string HTML for tracking icon |
529
|
|
|
*/ |
530
|
4 |
|
private function getTrackingIcon(string $table, TrackedTable|null $trackedTable): string |
531
|
|
|
{ |
532
|
4 |
|
$trackingIcon = ''; |
533
|
4 |
|
if (Tracker::isActive() && $trackedTable !== null) { |
534
|
|
|
$trackingIcon = $this->template->render('database/structure/tracking_icon', [ |
535
|
|
|
'db' => Current::$database, |
536
|
|
|
'table' => $table, |
537
|
|
|
'is_tracked' => $trackedTable->active, |
538
|
|
|
]); |
539
|
|
|
} |
540
|
|
|
|
541
|
4 |
|
return $trackingIcon; |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
/** |
545
|
|
|
* Returns whether the row count is approximated |
546
|
|
|
* |
547
|
|
|
* @param mixed[] $currentTable array containing details about the table |
548
|
|
|
* @param bool $tableIsView whether the table is a view |
549
|
|
|
* |
550
|
|
|
* @return array{bool, string} |
|
|
|
|
551
|
|
|
*/ |
552
|
4 |
|
private function isRowCountApproximated( |
553
|
|
|
array $currentTable, |
554
|
|
|
bool $tableIsView, |
555
|
|
|
): array { |
556
|
4 |
|
$approxRows = false; |
557
|
4 |
|
$showSuperscript = ''; |
558
|
|
|
|
559
|
|
|
// there is a null value in the ENGINE |
560
|
|
|
// - when the table needs to be repaired, or |
561
|
|
|
// - when it's a view |
562
|
|
|
// so ensure that we'll display "in use" below for a table |
563
|
|
|
// that needs to be repaired |
564
|
4 |
|
if (isset($currentTable['TABLE_ROWS']) && ($currentTable['ENGINE'] != null || $tableIsView)) { |
565
|
|
|
// InnoDB/TokuDB table: we did not get an accurate row count |
566
|
4 |
|
$approxRows = ! $tableIsView |
567
|
4 |
|
&& in_array($currentTable['ENGINE'], ['InnoDB', 'TokuDB'], true) |
568
|
4 |
|
&& ! $currentTable['COUNTED']; |
569
|
|
|
|
570
|
4 |
|
if ($tableIsView && $currentTable['TABLE_ROWS'] >= Config::getInstance()->settings['MaxExactCountViews']) { |
|
|
|
|
571
|
|
|
$approxRows = true; |
572
|
|
|
$showSuperscript = Generator::showHint( |
573
|
|
|
Sanitize::convertBBCode( |
574
|
|
|
sprintf( |
575
|
|
|
__( |
576
|
|
|
'This view has at least this number of rows. Please refer to %sdocumentation%s.', |
577
|
|
|
), |
578
|
|
|
'[doc@cfg_MaxExactCountViews]', |
579
|
|
|
'[/doc]', |
580
|
|
|
), |
581
|
|
|
), |
582
|
|
|
); |
583
|
|
|
} |
584
|
|
|
} |
585
|
|
|
|
586
|
4 |
|
return [$approxRows, $showSuperscript]; |
587
|
|
|
} |
588
|
|
|
|
589
|
|
|
/** |
590
|
|
|
* Returns the replication status of the table. |
591
|
|
|
* |
592
|
|
|
* @param mixed[] $replicaInfo |
593
|
|
|
* @param string $table table name |
594
|
|
|
* |
595
|
|
|
* @return array{bool, bool} |
|
|
|
|
596
|
|
|
*/ |
597
|
4 |
|
private function getReplicationStatus(array $replicaInfo, string $table): array |
598
|
|
|
{ |
599
|
4 |
|
$do = $ignored = false; |
600
|
4 |
|
if ($replicaInfo['status']) { |
601
|
|
|
$nbServReplicaDoDb = count($replicaInfo['Do_DB']); |
602
|
|
|
$nbServReplicaIgnoreDb = count($replicaInfo['Ignore_DB']); |
603
|
|
|
$searchDoDBInTruename = array_search($table, $replicaInfo['Do_DB']); |
604
|
|
|
$searchDoDBInDB = array_search(Current::$database, $replicaInfo['Do_DB']); |
605
|
|
|
|
606
|
|
|
$do = (is_string($searchDoDBInTruename) && $searchDoDBInTruename !== '') |
607
|
|
|
|| (is_string($searchDoDBInDB) && $searchDoDBInDB !== '') |
608
|
|
|
|| ($nbServReplicaDoDb == 0 && $nbServReplicaIgnoreDb == 0) |
609
|
|
|
|| $this->hasTable($replicaInfo['Wild_Do_Table'], $table); |
610
|
|
|
|
611
|
|
|
$searchDb = array_search(Current::$database, $replicaInfo['Ignore_DB']); |
612
|
|
|
$searchTable = array_search($table, $replicaInfo['Ignore_Table']); |
613
|
|
|
$ignored = (is_string($searchTable) && $searchTable !== '') |
614
|
|
|
|| (is_string($searchDb) && $searchDb !== '') |
615
|
|
|
|| $this->hasTable($replicaInfo['Wild_Ignore_Table'], $table); |
616
|
|
|
} |
617
|
|
|
|
618
|
4 |
|
return [$do, $ignored]; |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
/** |
622
|
|
|
* Find table with truename |
623
|
|
|
* |
624
|
|
|
* @param mixed[] $db DB to look into |
625
|
|
|
* @param string $truename Table name |
626
|
|
|
*/ |
627
|
4 |
|
private function hasTable(array $db, string $truename): bool |
628
|
|
|
{ |
629
|
4 |
|
foreach ($db as $dbTable) { |
630
|
|
|
if ( |
631
|
4 |
|
Current::$database == $this->replication->extractDbOrTable($dbTable) |
632
|
4 |
|
&& preg_match( |
633
|
4 |
|
'@^' . |
634
|
4 |
|
preg_quote(mb_substr($this->replication->extractDbOrTable($dbTable, 'table'), 0, -1), '@') . '@', |
635
|
4 |
|
$truename, |
636
|
4 |
|
) |
637
|
|
|
) { |
638
|
4 |
|
return true; |
639
|
|
|
} |
640
|
|
|
} |
641
|
|
|
|
642
|
4 |
|
return false; |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
/** |
646
|
|
|
* Get the value set for ENGINE table, |
647
|
|
|
* |
648
|
|
|
* @internal param bool $table_is_view whether table is view or not |
649
|
|
|
* |
650
|
|
|
* @param mixed[] $currentTable current table |
651
|
|
|
* @param int $sumSize total table size |
652
|
|
|
* @param int $overheadSize overhead size |
653
|
|
|
* |
654
|
|
|
* @return mixed[] |
655
|
|
|
*/ |
656
|
4 |
|
private function getStuffForEngineTypeTable( |
657
|
|
|
array $currentTable, |
658
|
|
|
int $sumSize, |
659
|
|
|
int $overheadSize, |
660
|
|
|
): array { |
661
|
4 |
|
$formattedSize = '-'; |
662
|
4 |
|
$unit = ''; |
663
|
4 |
|
$formattedOverhead = ''; |
664
|
4 |
|
$overheadUnit = ''; |
665
|
4 |
|
$tableIsView = false; |
666
|
|
|
|
667
|
4 |
|
switch ($currentTable['ENGINE']) { |
668
|
|
|
// MyISAM, ISAM or Heap table: Row count, data size and index size |
669
|
|
|
// are accurate; data size is accurate for ARCHIVE |
670
|
4 |
|
case 'MyISAM': |
671
|
4 |
|
case 'ISAM': |
672
|
4 |
|
case 'HEAP': |
673
|
4 |
|
case 'MEMORY': |
674
|
4 |
|
case 'ARCHIVE': |
675
|
4 |
|
case 'Aria': |
676
|
4 |
|
case 'Maria': |
677
|
4 |
|
[ |
678
|
4 |
|
$currentTable, |
679
|
4 |
|
$formattedSize, |
680
|
4 |
|
$unit, |
681
|
4 |
|
$formattedOverhead, |
682
|
4 |
|
$overheadUnit, |
683
|
4 |
|
$overheadSize, |
684
|
4 |
|
$sumSize, |
685
|
4 |
|
] = $this->getValuesForAriaTable( |
686
|
4 |
|
$currentTable, |
687
|
4 |
|
$sumSize, |
688
|
4 |
|
$overheadSize, |
689
|
4 |
|
$formattedSize, |
690
|
4 |
|
$unit, |
691
|
4 |
|
$formattedOverhead, |
692
|
4 |
|
$overheadUnit, |
693
|
4 |
|
); |
694
|
4 |
|
break; |
695
|
|
|
case 'InnoDB': |
696
|
|
|
case 'PBMS': |
697
|
|
|
case 'TokuDB': |
698
|
|
|
case 'ROCKSDB': |
699
|
|
|
// InnoDB table: Row count is not accurate but data and index sizes are. |
700
|
|
|
// PBMS table in Drizzle: TABLE_ROWS is taken from table cache, |
701
|
|
|
// so it may be unavailable |
702
|
|
|
[$currentTable, $formattedSize, $unit, $sumSize] = $this->getValuesForInnodbTable( |
703
|
|
|
$currentTable, |
704
|
|
|
$sumSize, |
705
|
|
|
); |
706
|
|
|
break; |
707
|
|
|
// Mysql 5.0.x (and lower) uses MRG_MyISAM |
708
|
|
|
// and MySQL 5.1.x (and higher) uses MRG_MYISAM |
709
|
|
|
// Both are aliases for MERGE |
710
|
|
|
case 'MRG_MyISAM': |
711
|
|
|
case 'MRG_MYISAM': |
712
|
|
|
case 'MERGE': |
713
|
|
|
case 'BerkeleyDB': |
714
|
|
|
// Merge or BerkleyDB table: Only row count is accurate. |
715
|
|
|
if ($this->isShowStats) { |
716
|
|
|
$formattedSize = ' - '; |
717
|
|
|
} |
718
|
|
|
|
719
|
|
|
break; |
720
|
|
|
// for a view, the ENGINE is sometimes reported as null, |
721
|
|
|
// or on some servers it's reported as "SYSTEM VIEW" |
722
|
|
|
case null: |
723
|
|
|
case 'SYSTEM VIEW': |
724
|
|
|
// possibly a view, do nothing |
725
|
|
|
break; |
726
|
|
|
case 'Mroonga': |
727
|
|
|
// The idea is to show the size only if Mroonga is available, |
728
|
|
|
// in other case the old unknown message will appear |
729
|
|
|
if (StorageEngine::hasMroongaEngine()) { |
730
|
|
|
[$currentTable, $formattedSize, $unit, $sumSize] = $this->getValuesForMroongaTable( |
731
|
|
|
$currentTable, |
732
|
|
|
$sumSize, |
733
|
|
|
); |
734
|
|
|
break; |
735
|
|
|
} |
736
|
|
|
// no break, go to default case |
737
|
|
|
default: |
738
|
|
|
// Unknown table type. |
739
|
|
|
if ($this->isShowStats) { |
740
|
|
|
$formattedSize = __('unknown'); |
741
|
|
|
} |
742
|
|
|
} |
743
|
|
|
|
744
|
4 |
|
if ($currentTable['TABLE_TYPE'] === 'VIEW' || $currentTable['TABLE_TYPE'] === 'SYSTEM VIEW') { |
745
|
|
|
// countRecords() takes care of $cfg['MaxExactCountViews'] |
746
|
|
|
$currentTable['TABLE_ROWS'] = $this->dbi |
747
|
|
|
->getTable(Current::$database, $currentTable['TABLE_NAME']) |
748
|
|
|
->countRecords(true); |
749
|
|
|
$tableIsView = true; |
750
|
|
|
} |
751
|
|
|
|
752
|
4 |
|
return [ |
753
|
4 |
|
$currentTable, |
754
|
4 |
|
$formattedSize, |
755
|
4 |
|
$unit, |
756
|
4 |
|
$formattedOverhead, |
757
|
4 |
|
$overheadUnit, |
758
|
4 |
|
$overheadSize, |
759
|
4 |
|
$tableIsView, |
760
|
4 |
|
$sumSize, |
761
|
4 |
|
]; |
762
|
|
|
} |
763
|
|
|
|
764
|
|
|
/** |
765
|
|
|
* Get values for ARIA/MARIA tables |
766
|
|
|
* |
767
|
|
|
* @param mixed[] $currentTable current table |
768
|
|
|
* @param int $sumSize sum size |
769
|
|
|
* @param int $overheadSize overhead size |
770
|
|
|
* @param string $formattedSize formatted size |
771
|
|
|
* @param string $unit unit |
772
|
|
|
* @param string $formattedOverhead overhead formatted |
773
|
|
|
* @param string $overheadUnit overhead unit |
774
|
|
|
* |
775
|
|
|
* @return mixed[] |
776
|
|
|
*/ |
777
|
8 |
|
private function getValuesForAriaTable( |
778
|
|
|
array $currentTable, |
779
|
|
|
int $sumSize, |
780
|
|
|
int $overheadSize, |
781
|
|
|
string $formattedSize, |
782
|
|
|
string $unit, |
783
|
|
|
string $formattedOverhead, |
784
|
|
|
string $overheadUnit, |
785
|
|
|
): array { |
786
|
8 |
|
if ($this->dbIsSystemSchema) { |
787
|
4 |
|
$currentTable['Rows'] = $this->dbi |
788
|
4 |
|
->getTable(Current::$database, $currentTable['Name']) |
789
|
4 |
|
->countRecords(); |
790
|
|
|
} |
791
|
|
|
|
792
|
8 |
|
if ($this->isShowStats) { |
793
|
|
|
/** @var int $tblsize */ |
794
|
8 |
|
$tblsize = $currentTable['Data_length'] |
795
|
8 |
|
+ $currentTable['Index_length']; |
796
|
8 |
|
$sumSize += $tblsize; |
797
|
8 |
|
[$formattedSize, $unit] = Util::formatByteDown($tblsize, 3, $tblsize > 0 ? 1 : 0); |
798
|
8 |
|
if (isset($currentTable['Data_free']) && $currentTable['Data_free'] > 0) { |
799
|
8 |
|
[$formattedOverhead, $overheadUnit] = Util::formatByteDown($currentTable['Data_free'], 3, 1); |
800
|
8 |
|
$overheadSize += $currentTable['Data_free']; |
801
|
|
|
} |
802
|
|
|
} |
803
|
|
|
|
804
|
8 |
|
return [$currentTable, $formattedSize, $unit, $formattedOverhead, $overheadUnit, $overheadSize, $sumSize]; |
805
|
|
|
} |
806
|
|
|
|
807
|
|
|
/** |
808
|
|
|
* Get values for InnoDB table |
809
|
|
|
* |
810
|
|
|
* @param mixed[] $currentTable current table |
811
|
|
|
* @param int $sumSize sum size |
812
|
|
|
* |
813
|
|
|
* @return mixed[] |
814
|
|
|
*/ |
815
|
4 |
|
private function getValuesForInnodbTable( |
816
|
|
|
array $currentTable, |
817
|
|
|
int $sumSize, |
818
|
|
|
): array { |
819
|
4 |
|
$formattedSize = $unit = ''; |
820
|
|
|
|
821
|
|
|
if ( |
822
|
4 |
|
(in_array($currentTable['ENGINE'], ['InnoDB', 'TokuDB'], true) |
823
|
4 |
|
&& $currentTable['TABLE_ROWS'] < Config::getInstance()->settings['MaxExactCount']) |
|
|
|
|
824
|
4 |
|
|| ! isset($currentTable['TABLE_ROWS']) |
825
|
|
|
) { |
826
|
4 |
|
$currentTable['COUNTED'] = true; |
827
|
4 |
|
$currentTable['TABLE_ROWS'] = $this->dbi |
828
|
4 |
|
->getTable(Current::$database, $currentTable['TABLE_NAME']) |
829
|
4 |
|
->countRecords(true); |
830
|
|
|
} else { |
831
|
4 |
|
$currentTable['COUNTED'] = false; |
832
|
|
|
} |
833
|
|
|
|
834
|
4 |
|
if ($this->isShowStats) { |
835
|
|
|
/** @var int $tblsize */ |
836
|
4 |
|
$tblsize = $currentTable['Data_length'] |
837
|
4 |
|
+ $currentTable['Index_length']; |
838
|
4 |
|
$sumSize += $tblsize; |
839
|
4 |
|
[$formattedSize, $unit] = Util::formatByteDown($tblsize, 3, $tblsize > 0 ? 1 : 0); |
840
|
|
|
} |
841
|
|
|
|
842
|
4 |
|
return [$currentTable, $formattedSize, $unit, $sumSize]; |
843
|
|
|
} |
844
|
|
|
|
845
|
|
|
/** |
846
|
|
|
* Get values for Mroonga table |
847
|
|
|
* |
848
|
|
|
* @param mixed[] $currentTable current table |
849
|
|
|
* @param int $sumSize sum size |
850
|
|
|
* |
851
|
|
|
* @return mixed[] |
852
|
|
|
*/ |
853
|
4 |
|
private function getValuesForMroongaTable( |
854
|
|
|
array $currentTable, |
855
|
|
|
int $sumSize, |
856
|
|
|
): array { |
857
|
4 |
|
$formattedSize = ''; |
858
|
4 |
|
$unit = ''; |
859
|
|
|
|
860
|
4 |
|
if ($this->isShowStats) { |
861
|
|
|
/** @var int $tblsize */ |
862
|
4 |
|
$tblsize = $currentTable['Data_length'] + $currentTable['Index_length']; |
863
|
4 |
|
$sumSize += $tblsize; |
864
|
4 |
|
[$formattedSize, $unit] = Util::formatByteDown($tblsize, 3, $tblsize > 0 ? 1 : 0); |
865
|
|
|
} |
866
|
|
|
|
867
|
4 |
|
return [$currentTable, $formattedSize, $unit, $sumSize]; |
868
|
|
|
} |
869
|
|
|
} |
870
|
|
|
|
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths