mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
add Pg table viewer from list
This commit is contained in:
40
app/DB/AbstractTable.php
Normal file
40
app/DB/AbstractTable.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gazelle\DB;
|
||||
|
||||
abstract class AbstractTable extends \Gazelle\BaseObject {
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
) {}
|
||||
|
||||
public function link(): string {
|
||||
return "<a href=\"{$this->url()}\">{$this->name}</a>";
|
||||
}
|
||||
|
||||
/* The usual design pattern would be to have a Table manager
|
||||
* and look up the table by name. But that would add a fair
|
||||
* amount of effort for little gain, so instead an exists()
|
||||
* method can be used to validate the object does in fact
|
||||
* point a real table.
|
||||
*/
|
||||
abstract public function exists(): bool;
|
||||
|
||||
/* The CREATE TABLE string */
|
||||
abstract public function definition(): string;
|
||||
|
||||
/* metadata about index reads */
|
||||
abstract public function indexRead(): array;
|
||||
|
||||
/* metadata about table reads */
|
||||
abstract public function tableRead(): array;
|
||||
|
||||
/* general statistics of the table */
|
||||
abstract public function stats(): array;
|
||||
|
||||
/* none of the derived classes perform any caching */
|
||||
public function flush(): static {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
87
app/DB/MysqlTable.php
Normal file
87
app/DB/MysqlTable.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gazelle\DB;
|
||||
|
||||
class MysqlTable extends AbstractTable {
|
||||
protected Mysql $dbro;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
) {
|
||||
$this->dbro = \Gazelle\DB::DB(readWrite: false);
|
||||
}
|
||||
|
||||
public function location(): string {
|
||||
return "tools.php?action=db-mysql&table={$this->name}";
|
||||
}
|
||||
|
||||
public function exists(): bool {
|
||||
return (bool)self::$db->scalar("
|
||||
SELECT 1
|
||||
FROM information_schema.tables t
|
||||
WHERE t.table_schema = ?
|
||||
AND t.table_name = ?
|
||||
", MYSQL_DB, $this->name
|
||||
);
|
||||
}
|
||||
|
||||
public function definition(): string {
|
||||
return self::$db->row("SHOW CREATE TABLE {$this->name}")[1];
|
||||
}
|
||||
|
||||
public function indexRead(): array {
|
||||
self::$db->prepared_query("
|
||||
SELECT s.INDEX_NAME AS index_name,
|
||||
coalesce(si.ROWS_READ, 0) AS rows_read,
|
||||
group_concat(
|
||||
concat(s.column_name, ' {', s.cardinality, '}')
|
||||
ORDER BY s.seq_in_index
|
||||
SEPARATOR ', '
|
||||
) AS column_list
|
||||
FROM information_schema.statistics s
|
||||
LEFT JOIN information_schema.index_statistics si
|
||||
USING (TABLE_SCHEMA, TABLE_NAME, INDEX_NAME)
|
||||
WHERE s.TABLE_SCHEMA = ?
|
||||
AND s.TABLE_NAME = ?
|
||||
GROUP BY index_name,
|
||||
rows_read
|
||||
ORDER BY s.TABLE_NAME,
|
||||
s.INDEX_NAME = 'PRIMARY' DESC,
|
||||
coalesce(si.ROWS_READ, 0) DESC,
|
||||
s.INDEX_NAME
|
||||
", MYSQL_DB, $this->name
|
||||
);
|
||||
return self::$db->to_array(false, MYSQLI_ASSOC, false);
|
||||
}
|
||||
|
||||
public function tableRead(): array {
|
||||
return self::$db->rowAssoc("
|
||||
SELECT ROWS_READ, ROWS_CHANGED, ROWS_CHANGED_X_INDEXES
|
||||
FROM information_schema.table_statistics
|
||||
WHERE TABLE_SCHEMA = ?
|
||||
AND TABLE_NAME = ?
|
||||
", MYSQL_DB, $this->name
|
||||
);
|
||||
}
|
||||
|
||||
public function stats(): array {
|
||||
return self::$db->rowAssoc("
|
||||
SELECT t.TABLE_ROWS,
|
||||
t.AVG_ROW_LENGTH,
|
||||
t.DATA_LENGTH,
|
||||
t.INDEX_LENGTH,
|
||||
t.DATA_FREE,
|
||||
ts.ROWS_READ,
|
||||
ts.ROWS_CHANGED,
|
||||
ts.ROWS_CHANGED_X_INDEXES
|
||||
FROM information_schema.tables t
|
||||
INNER JOIN information_schema.table_statistics ts
|
||||
USING (TABLE_SCHEMA, TABLE_NAME)
|
||||
WHERE t.TABLE_SCHEMA = ?
|
||||
AND t.TABLE_NAME = ?
|
||||
", MYSQL_DB, $this->name
|
||||
);
|
||||
}
|
||||
}
|
||||
156
app/DB/PgTable.php
Normal file
156
app/DB/PgTable.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gazelle\DB;
|
||||
|
||||
class PgTable extends AbstractTable {
|
||||
public function location(): string {
|
||||
return "tools.php?action=db-pg&table={$this->name}";
|
||||
}
|
||||
|
||||
protected function schemaTable(): array {
|
||||
/* If we have 'schemaname.tablename', split them apart and
|
||||
* return them separately. Otherwise we have only a table
|
||||
* name, which is assumed to be in the public schema.
|
||||
*/
|
||||
$all = explode('.', $this->name, 2);
|
||||
if (count($all) === 2) {
|
||||
return $all;
|
||||
}
|
||||
return ['public', $this->name];
|
||||
}
|
||||
|
||||
public function exists(): bool {
|
||||
[$schema, $table] = $this->schemaTable();
|
||||
return (bool)$this->pg()->scalar("
|
||||
SELECT 1
|
||||
FROM information_schema.tables t
|
||||
WHERE t.table_schema = ?
|
||||
AND t.table_name = ?
|
||||
", $schema, $table
|
||||
);
|
||||
}
|
||||
|
||||
public function definition(): string {
|
||||
[$schema, $table] = $this->schemaTable();
|
||||
/* Generating a create table in Postgresql is non-trivial
|
||||
* unless you have a pg_dump binary handy (and wish to
|
||||
* spawn a child process). So we use some code a person
|
||||
* on the internet wrote, and bend it to our needs.
|
||||
*/
|
||||
return implode(
|
||||
"\n",
|
||||
$this->pgro()->column("
|
||||
with pkey as (
|
||||
select cc.conrelid,
|
||||
format(E',
|
||||
constraint %I primary key(%s)', cc.conname,
|
||||
string_agg(a.attname, ', '
|
||||
order by array_position(cc.conkey, a.attnum))
|
||||
) pkey
|
||||
from pg_catalog.pg_constraint cc
|
||||
inner join pg_catalog.pg_class c on (c.oid = cc.conrelid)
|
||||
inner join pg_catalog.pg_attribute a on (
|
||||
a.attrelid = cc.conrelid and a.attnum = any(cc.conkey)
|
||||
)
|
||||
where cc.contype = 'p'
|
||||
group by cc.conrelid, cc.conname
|
||||
)
|
||||
select format(E'create %stable %s%I (\n%s%s\n);\n',
|
||||
case c.relpersistence when 't' then 'temporary ' else '' end,
|
||||
case c.relpersistence when 't' then '' else n.nspname || '.' end,
|
||||
c.relname,
|
||||
string_agg(
|
||||
format(E'\t%I %s%s',
|
||||
a.attname,
|
||||
pg_catalog.format_type(a.atttypid, a.atttypmod),
|
||||
case when a.attnotnull then ' not null' else '' end
|
||||
), E',\n'
|
||||
order by a.attnum
|
||||
),
|
||||
(select pkey from pkey where pkey.conrelid = c.oid)
|
||||
) as sql
|
||||
from pg_catalog.pg_class c
|
||||
inner join pg_catalog.pg_namespace n on (n.oid = c.relnamespace)
|
||||
inner join pg_catalog.pg_attribute a on (a.attrelid = c.oid and a.attnum > 0)
|
||||
inner join pg_catalog.pg_type t on (a.atttypid = t.oid)
|
||||
where n.nspname = ?
|
||||
and c.relname = ?
|
||||
group by c.oid, c.relname, c.relpersistence, n.nspname;
|
||||
", $schema, $table
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function indexRead(): array {
|
||||
[$schema, $table] = $this->schemaTable();
|
||||
return $this->pgro()->all("
|
||||
select indexrelname,
|
||||
idx_scan,
|
||||
idx_tup_read,
|
||||
idx_tup_fetch,
|
||||
last_idx_scan
|
||||
from pg_stat_all_indexes
|
||||
where schemaname = ?
|
||||
and relname = ?
|
||||
", $schema, $table
|
||||
);
|
||||
}
|
||||
|
||||
public function tableRead(): array {
|
||||
[$schema, $table] = $this->schemaTable();
|
||||
return $this->pgro()->rowAssoc("
|
||||
select seq_scan,
|
||||
last_seq_scan,
|
||||
seq_tup_read,
|
||||
idx_scan,
|
||||
last_idx_scan,
|
||||
idx_tup_fetch,
|
||||
n_tup_ins,
|
||||
n_tup_upd,
|
||||
n_tup_del,
|
||||
n_tup_hot_upd,
|
||||
n_tup_newpage_upd,
|
||||
n_live_tup,
|
||||
n_dead_tup,
|
||||
n_ins_since_vacuum,
|
||||
last_vacuum,
|
||||
last_autovacuum,
|
||||
vacuum_count,
|
||||
autovacuum_count,
|
||||
n_mod_since_analyze,
|
||||
analyze_count,
|
||||
autoanalyze_count
|
||||
from pg_stat_user_tables
|
||||
where schemaname = ?
|
||||
and relname = ?
|
||||
", $schema, $table
|
||||
);
|
||||
}
|
||||
|
||||
public function stats(): array {
|
||||
[$schema, $table] = $this->schemaTable();
|
||||
return $this->pg()->rowAssoc("
|
||||
select pg_relation_size(t.table_schema || '.' || t.table_name) as table_size,
|
||||
pg_indexes_size(t.table_schema || '.' || t.table_name) as index_size,
|
||||
s.n_live_tup as live,
|
||||
s.n_dead_tup as dead,
|
||||
case when s.n_dead_tup + s.n_live_tup = 0
|
||||
then 0
|
||||
else round(s.n_dead_tup/(s.n_dead_tup + n_live_tup*1.0), 5)
|
||||
end as dead_ratio,
|
||||
now() - s.last_autoanalyze as analyze_delta,
|
||||
now() - s.last_autovacuum as vacuum_delta,
|
||||
s.autoanalyze_count as analyze_total,
|
||||
s.autovacuum_count as vacuum_total
|
||||
from information_schema.tables t
|
||||
inner join pg_stat_user_tables s on (
|
||||
s.schemaname = t.table_schema and s.relname = t.table_name
|
||||
)
|
||||
where t.table_schema = ?
|
||||
and t.table_name = ?
|
||||
", $schema, $table
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,40 +6,30 @@ declare(strict_types=1);
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
use Gazelle\Enum\Direction;
|
||||
use Gazelle\Enum\MysqlInfoOrderBy;
|
||||
use Gazelle\Enum\MysqlTableMode;
|
||||
|
||||
if (!$Viewer->permitted('site_database_specifics')) {
|
||||
Error403::error();
|
||||
}
|
||||
|
||||
// View table definition
|
||||
$db = DB::DB();
|
||||
if (!empty($_GET['table']) && preg_match('/([\w-]+)/', $_GET['table'], $match)) {
|
||||
$tableName = $match[1];
|
||||
$siteInfo = new SiteInfo();
|
||||
if (!$siteInfo->tableExists($tableName)) {
|
||||
Error404::error("No such table");
|
||||
if (preg_match('/([\w-]+)/', $_GET['table'] ?? '', $match)) {
|
||||
$table = new DB\MysqlTable($match[1]);
|
||||
if (!$table->exists()) {
|
||||
Error404::error("No such Mysql table {$match[1]}");
|
||||
}
|
||||
echo $Twig->render('admin/mysql-table.twig', [
|
||||
'definition' => $db->row('SHOW CREATE TABLE ' . $tableName)[1],
|
||||
'table_name' => $tableName,
|
||||
'table_read' => $siteInfo->tableRowsRead($tableName),
|
||||
'index_read' => $siteInfo->indexRowsRead($tableName),
|
||||
'stats' => $siteInfo->tableStats($tableName),
|
||||
'table' => $table,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$info = (new DB\MysqlInfo(
|
||||
DB\MysqlInfo::lookupTableMode($_GET['mode'] ?? MysqlTableMode::all->value),
|
||||
DB\MysqlInfo::lookupOrderby($_GET['order'] ?? MysqlInfoOrderBy::tableName->value),
|
||||
DB::lookupDirection($_GET['sort'] ?? Direction::ascending->value))
|
||||
DB\MysqlInfo::lookupTableMode($_GET['mode'] ?? Enum\MysqlTableMode::all->value),
|
||||
DB\MysqlInfo::lookupOrderby($_GET['order'] ?? Enum\MysqlInfoOrderBy::tableName->value),
|
||||
DB::lookupDirection($_GET['sort'] ?? Enum\Direction::ascending->value))
|
||||
);
|
||||
$list = $info->info();
|
||||
$column = $info->orderBy() == MysqlInfoOrderBy::tableName
|
||||
? MysqlInfoOrderBy::tableRows->value
|
||||
$column = $info->orderBy() == Enum\MysqlInfoOrderBy::tableName
|
||||
? Enum\MysqlInfoOrderBy::tableRows->value
|
||||
: $info->orderBy()->value;
|
||||
$data = [];
|
||||
foreach ($list as $t) {
|
||||
@@ -48,7 +38,7 @@ foreach ($list as $t) {
|
||||
|
||||
echo $Twig->render('admin/mysql-table-summary.twig', [
|
||||
'header' => new Util\SortableTableHeader(
|
||||
MysqlInfoOrderBy::tableName->value,
|
||||
Enum\MysqlInfoOrderBy::tableName->value,
|
||||
DB\MysqlInfo::columnList(),
|
||||
),
|
||||
'list' => $list,
|
||||
|
||||
@@ -6,21 +6,30 @@ declare(strict_types=1);
|
||||
|
||||
namespace Gazelle;
|
||||
|
||||
use Gazelle\Enum\PgInfoOrderBy;
|
||||
use Gazelle\Enum\Direction;
|
||||
|
||||
if (!$Viewer->permitted('site_database_specifics')) {
|
||||
Error403::error();
|
||||
}
|
||||
|
||||
// View table definition
|
||||
if (preg_match('/([\w-]+(?:\.[\w-]+)?)/', $_GET['table'] ?? '', $match)) {
|
||||
$table = new DB\PgTable($match[1]);
|
||||
if (!$table->exists()) {
|
||||
Error404::error("No such Postgresql table {$match[1]}");
|
||||
}
|
||||
echo $Twig->render('admin/pg-table.twig', [
|
||||
'table' => $table,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$info = new DB\PgInfo(
|
||||
DB\PgInfo::lookupOrderby($_GET['order'] ?? PgInfoOrderBy::tableName->value),
|
||||
DB::lookupDirection($_GET['sort'] ?? Direction::ascending->value)
|
||||
DB\PgInfo::lookupOrderby($_GET['order'] ?? Enum\PgInfoOrderBy::tableName->value),
|
||||
DB::lookupDirection($_GET['sort'] ?? Enum\Direction::ascending->value)
|
||||
);
|
||||
|
||||
echo $Twig->render('admin/pg-table-summary.twig', [
|
||||
'header' => new Util\SortableTableHeader(
|
||||
PgInfoOrderBy::tableName->value,
|
||||
Enum\PgInfoOrderBy::tableName->value,
|
||||
DB\PgInfo::columnList()
|
||||
),
|
||||
'list' => $info->info(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{ header('Database Specifics - ' ~ table_name) }}
|
||||
{{ header('Database Specifics - ' ~ table.name) }}
|
||||
<div class="linkbox">
|
||||
<a href="tools.php?action=service_stats" class="brackets">Cache/DB stats</a>
|
||||
<a href="tools.php?action=clear_cache" class="brackets">Cache inspector</a>
|
||||
@@ -8,9 +8,9 @@
|
||||
</div>
|
||||
|
||||
<div class="pad"><div class="box pad">
|
||||
<h3>Table {{ table_name }} definition</h3>
|
||||
<pre>{{ definition }}</pre>
|
||||
<a href="tools.php?action=db_sandbox&table={{ table_name }}" class="brackets">Inspect</a>
|
||||
<h3>Mysql table {{ table.name }} definition</h3>
|
||||
<pre>{{ table.definition }}</pre>
|
||||
<a href="tools.php?action=db_sandbox&table={{ table.name }}" class="brackets">Inspect</a>
|
||||
</div></div>
|
||||
|
||||
<div class="pad"><div class="box pad">
|
||||
@@ -26,18 +26,16 @@
|
||||
<th>Rows changed</th>
|
||||
<th>Rows changed per index</th>
|
||||
</tr>
|
||||
{% for r in table_read %}
|
||||
<tr>
|
||||
<td>{{ stats.TABLE_ROWS|number_format }}</td>
|
||||
<td>{{ stats.AVG_ROW_LENGTH|number_format }}</td>
|
||||
<td>{{ stats.DATA_LENGTH|octet_size }}</td>
|
||||
<td>{{ stats.INDEX_LENGTH|octet_size }}</td>
|
||||
<td>{{ stats.DATA_FREE|octet_size }}</td>
|
||||
<td>{{ stats.ROWS_READ|number_format }}</td>
|
||||
<td>{{ stats.ROWS_CHANGED|number_format }}</td>
|
||||
<td>{{ stats.ROWS_CHANGED_X_INDEXES|number_format }}</td>
|
||||
<td>{{ table.stats.TABLE_ROWS|number_format }}</td>
|
||||
<td>{{ table.stats.AVG_ROW_LENGTH|number_format }}</td>
|
||||
<td>{{ table.stats.DATA_LENGTH|octet_size }}</td>
|
||||
<td>{{ table.stats.INDEX_LENGTH|octet_size }}</td>
|
||||
<td>{{ table.stats.DATA_FREE|octet_size }}</td>
|
||||
<td>{{ table.stats.ROWS_READ|number_format }}</td>
|
||||
<td>{{ table.stats.ROWS_CHANGED|number_format }}</td>
|
||||
<td>{{ table.stats.ROWS_CHANGED_X_INDEXES|number_format }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div></div>
|
||||
|
||||
@@ -49,7 +47,7 @@
|
||||
<th>Rows read</th>
|
||||
<th>Column list and cardinalities</th>
|
||||
</tr>
|
||||
{% for r in index_read %}
|
||||
{% for r in table.indexRead %}
|
||||
<tr>
|
||||
<td>{{ r.index_name }}</td>
|
||||
<td>{{ r.rows_read|number_format }}</td>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pad"><div class="box pad">
|
||||
<h3>Rows read</h3>
|
||||
<h3>Postgresql tables</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ header|column('table_name') }}</th>
|
||||
@@ -23,7 +23,8 @@
|
||||
</tr>
|
||||
{% for r in list %}
|
||||
<tr>
|
||||
<td>{{ r.table_name }}</td>
|
||||
<td><a href="tools.php?action=db-pg&table={{ r.table_name }}">{{
|
||||
r.table_name }}</a></td>
|
||||
<td>{{ r.table_size|octet_size }}</td>
|
||||
<td>{{ r.index_size|octet_size }}</td>
|
||||
<td>{{ r.live|number_format }}</td>
|
||||
|
||||
70
templates/admin/pg-table.twig
Normal file
70
templates/admin/pg-table.twig
Normal file
@@ -0,0 +1,70 @@
|
||||
{{ header('Database Specifics - Postgresql ' ~ table.name) }}
|
||||
<div class="linkbox">
|
||||
<a href="tools.php?action=service_stats" class="brackets">Cache/DB stats</a>
|
||||
<a href="tools.php?action=clear_cache" class="brackets">Cache inspector</a>
|
||||
<a href="tools.php?action=db-mysql" class="brackets">Mysql inspector</a>
|
||||
<a href="tools.php?action=db-pg" class="brackets">Postgresql inspector</a>
|
||||
<a href="tools.php?action=db_sandbox" class="brackets">DB sandbox</a>
|
||||
</div>
|
||||
|
||||
<div class="pad"><div class="box pad">
|
||||
<h3>Postgresql table {{ table.name }} definition</h3>
|
||||
<pre>{{ table.definition }}</pre>
|
||||
<a href="tools.php?action=db_sandbox&src=pg&table={{ table.name }}" class="brackets">Inspect</a>
|
||||
</div></div>
|
||||
|
||||
<div class="pad"><div class="box pad">
|
||||
<h3>Rows read</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Live tuples</th>
|
||||
<th>Dead tuples</th>
|
||||
<th>Ratio</th>
|
||||
<th>Table</th>
|
||||
<th>Index</th>
|
||||
<th>Analyzed</th>
|
||||
<th>Vacuumed</th>
|
||||
<th>Analyze total</th>
|
||||
<th>Vacuum total</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ table.stats.live|number_format }}</td>
|
||||
<td>{{ table.stats.dead|number_format }}</td>
|
||||
<td>{{ (table.stats.dead == 0
|
||||
? 0
|
||||
: (table.stats.dead / table.stats.live) * 100)|number_format(2)
|
||||
}}</td>
|
||||
<td>{{ table.stats.table_size|octet_size }}</td>
|
||||
<td>{{ table.stats.index_size|octet_size }}</td>
|
||||
<td>{{ table.stats.analyze_delta|time_diff }}</td>
|
||||
<td>{{ table.stats.vacuum_delta|time_diff }}</td>
|
||||
<td>{{ table.stats.analyze_total|number_format }}</td>
|
||||
<td>{{ table.stats.vacuum_total|number_format }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div></div>
|
||||
|
||||
<div class="pad"><div class="box pad">
|
||||
<h3>Index reads</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Index name</th>
|
||||
<th>Calls</th>
|
||||
<th>Tuples read</th>
|
||||
<th>Tuples fetched</th>
|
||||
<th>Last use</th>
|
||||
<th>Column list and cardinalities</th>
|
||||
</tr>
|
||||
{% for r in table.indexRead %}
|
||||
<tr>
|
||||
<td>{{ r.indexrelname }}</td>
|
||||
<td>{{ r.idx_scan|number_format }}</td>
|
||||
<td>{{ r.idx_tup_read|number_format }}</td>
|
||||
<td>{{ r.idx_tup_fetch|number_format }}</td>
|
||||
<td>{{ r.last_idx_scan|time_diff }}</td>
|
||||
<td>todo</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div></div>
|
||||
{{ footer() }}
|
||||
@@ -5,7 +5,6 @@ namespace Gazelle;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use GazelleUnitTest\Helper;
|
||||
use Gazelle\Enum\Direction;
|
||||
|
||||
class DbTest extends TestCase {
|
||||
use Pg;
|
||||
@@ -279,6 +278,121 @@ class DbTest extends TestCase {
|
||||
$this->assertArrayHasKey('epoch', $errorList[0], 'db-pg-error-epoch');
|
||||
}
|
||||
|
||||
public function testMysqlDuplicateException(): void {
|
||||
$this->expectException(DB\MysqlDuplicateKeyException::class);
|
||||
DB::DB()->prepared_query("
|
||||
INSERT INTO users_main
|
||||
(ID, Username, Email, PassHash, torrent_pass, IP, PermissionID, Enabled, Invites, ipcc, auth_key, stylesheet_id)
|
||||
VALUES (1, 'phpunit', '', '', '', '', 0, '0', 0, '', '', 0)
|
||||
");
|
||||
}
|
||||
|
||||
public function testMysqlTable(): void {
|
||||
$bad = new DB\MysqlTable('nosuchtable');
|
||||
$this->assertFalse($bad->exists(), 'mysql-table-does-not-exist');
|
||||
|
||||
$table = new DB\MysqlTable('users_main');
|
||||
$this->assertTrue($table->exists(), 'mysql-table-exists');
|
||||
$this->assertEquals(
|
||||
"tools.php?action=db-mysql&table={$table->name}",
|
||||
$table->location(),
|
||||
'mysql-table-location',
|
||||
);
|
||||
$this->assertEquals(
|
||||
"<a href=\"tools.php?action=db-mysql&table=users_main\">users_main</a>",
|
||||
$table->link(),
|
||||
'mysql-table-link',
|
||||
);
|
||||
$this->assertStringStartsWith(
|
||||
'CREATE TABLE `' . $table->name . '` (',
|
||||
$table->definition(),
|
||||
'mysql-table-definition',
|
||||
);
|
||||
|
||||
// if this fails, check the permissions on information_schema.table_statistics
|
||||
$this->assertEquals(
|
||||
[
|
||||
"ROWS_READ", "ROWS_CHANGED", "ROWS_CHANGED_X_INDEXES",
|
||||
],
|
||||
array_keys($table->tableRead()),
|
||||
'mysql-table-table-read',
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
"index_name", "rows_read", "column_list",
|
||||
],
|
||||
array_keys($table->indexRead()[0]),
|
||||
'mysql-table-index-read',
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
"TABLE_ROWS", "AVG_ROW_LENGTH", "DATA_LENGTH", "INDEX_LENGTH",
|
||||
"DATA_FREE", "ROWS_READ", "ROWS_CHANGED", "ROWS_CHANGED_X_INDEXES",
|
||||
],
|
||||
array_keys($table->stats()),
|
||||
'mysql-table-stats',
|
||||
);
|
||||
}
|
||||
|
||||
public function testPgTable(): void {
|
||||
$bad = new DB\PgTable('nosuchtable');
|
||||
$this->assertFalse($bad->exists(), 'pg-table-does-not-exist');
|
||||
|
||||
$table = new DB\PgTable('user_audit_trail');
|
||||
$this->assertTrue($table->exists(), 'pg-table-exists');
|
||||
$this->assertEquals(
|
||||
"tools.php?action=db-pg&table={$table->name}",
|
||||
$table->location(),
|
||||
'pg-table-location',
|
||||
);
|
||||
$this->assertEquals(
|
||||
"<a href=\"tools.php?action=db-pg&table=user_audit_trail\">user_audit_trail</a>",
|
||||
$table->link(),
|
||||
'pg-table-link',
|
||||
);
|
||||
$this->assertStringStartsWith(
|
||||
"create table public.{$table->name} (",
|
||||
$table->definition(),
|
||||
'pg-table-definition',
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
"seq_scan", "last_seq_scan", "seq_tup_read", "idx_scan",
|
||||
"last_idx_scan", "idx_tup_fetch", "n_tup_ins", "n_tup_upd",
|
||||
"n_tup_del", "n_tup_hot_upd", "n_tup_newpage_upd",
|
||||
"n_live_tup", "n_dead_tup", "n_ins_since_vacuum",
|
||||
"last_vacuum", "last_autovacuum", "vacuum_count",
|
||||
"autovacuum_count", "n_mod_since_analyze", "analyze_count",
|
||||
"autoanalyze_count",
|
||||
],
|
||||
array_keys($table->tableRead()),
|
||||
'pg-table-table-read',
|
||||
);
|
||||
|
||||
$indexRead = $table->indexRead();
|
||||
$this->assertCount(4, $indexRead, 'pg-table-index-read-total');
|
||||
$index = $indexRead[0];
|
||||
$this->assertEquals(
|
||||
[
|
||||
"indexrelname", "idx_scan", "idx_tup_read",
|
||||
"idx_tup_fetch", "last_idx_scan",
|
||||
],
|
||||
array_keys($index),
|
||||
'pg-table-index-read'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
"table_size", "index_size", "live", "dead", "dead_ratio",
|
||||
"analyze_delta", "vacuum_delta", "analyze_total",
|
||||
"vacuum_total",
|
||||
],
|
||||
array_keys($table->stats()),
|
||||
'pg-table-stats',
|
||||
);
|
||||
}
|
||||
|
||||
public function testPgByteaScalar(): void {
|
||||
$this->pg()->prepared_query("
|
||||
create temporary table test_bytea (
|
||||
|
||||
@@ -14,6 +14,10 @@ class MysqlInfoTest extends TestCase {
|
||||
$this->assertEquals(MysqlInfoOrderBy::tableName, DB\MysqlInfo::lookupOrderby('wut'), 'mysqlfo-orderby-default');
|
||||
}
|
||||
|
||||
public function testMysqlInfoColumn(): void {
|
||||
$this->assertCount(9, DB\MysqlInfo::columnList(), 'myinfo-column-list');
|
||||
}
|
||||
|
||||
public function testMysqlInfoList(): void {
|
||||
$mysqlInfo = new DB\MysqlInfo(
|
||||
MysqlTableMode::all,
|
||||
|
||||
@@ -3,25 +3,23 @@
|
||||
namespace Gazelle;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Gazelle\Enum\Direction;
|
||||
use Gazelle\Enum\PgInfoOrderBy;
|
||||
|
||||
class PgInfoTest extends TestCase {
|
||||
use Pg;
|
||||
|
||||
public function testDirection(): void {
|
||||
$this->assertEquals(
|
||||
PgInfoOrderBy::tableSize,
|
||||
Enum\PgInfoOrderBy::tableSize,
|
||||
DB\PgInfo::lookupOrderby('table_size'),
|
||||
'pginfo-orderby-tablesize'
|
||||
);
|
||||
$this->assertEquals(
|
||||
PgInfoOrderBy::tableName,
|
||||
Enum\PgInfoOrderBy::tableName,
|
||||
DB\PgInfo::lookupOrderby('table_name'),
|
||||
'pginfo-orderby-tablename'
|
||||
);
|
||||
$this->assertEquals(
|
||||
PgInfoOrderBy::tableName,
|
||||
Enum\PgInfoOrderBy::tableName,
|
||||
DB\PgInfo::lookupOrderby('wut'),
|
||||
'pginfo-orderby-default'
|
||||
);
|
||||
@@ -29,13 +27,17 @@ class PgInfoTest extends TestCase {
|
||||
|
||||
public function testPgInfoList(): void {
|
||||
$pgInfo = new DB\PgInfo(
|
||||
PgInfoOrderBy::tableName,
|
||||
Direction::descending,
|
||||
Enum\PgInfoOrderBy::tableName,
|
||||
Enum\Direction::descending,
|
||||
);
|
||||
$list = $pgInfo->info();
|
||||
$this->assertEquals('public.user_warning', $list[0]['table_name'], 'pginfo-list');
|
||||
}
|
||||
|
||||
public function testPgInfoColumn(): void {
|
||||
$this->assertCount(10, DB\PgInfo::columnList(), 'pginfo-column-list');
|
||||
}
|
||||
|
||||
public function testCheckpointInfo(): void {
|
||||
$info = $this->pg()->checkpointInfo();
|
||||
$this->assertCount(3, $info);
|
||||
|
||||
Reference in New Issue
Block a user