diff --git a/app/DB/PgInfo.php b/app/DB/PgInfo.php index faf32de32..26871ad97 100644 --- a/app/DB/PgInfo.php +++ b/app/DB/PgInfo.php @@ -15,7 +15,8 @@ class PgInfo { public function info(): array { return $this->pg()->all(" - select t.table_schema || '.' || t.table_name as table_name, + select (case when t.table_schema = 'public' then '' else t.table_schema || '.' end ) + || t.table_name as table_name, 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, diff --git a/app/DB/PgTable.php b/app/DB/PgTable.php index d0aa054e3..7de11b8b0 100644 --- a/app/DB/PgTable.php +++ b/app/DB/PgTable.php @@ -44,8 +44,7 @@ class PgTable extends AbstractTable { $this->pgro()->column(" with pkey as ( select cc.conrelid, - format(E', - constraint %I primary key(%s)', cc.conname, + format(E',\n constraint %I primary key(%s)', cc.conname, string_agg(a.attname, ', ' order by array_position(cc.conkey, a.attnum)) ) pkey @@ -62,7 +61,7 @@ class PgTable extends AbstractTable { case c.relpersistence when 't' then '' else n.nspname || '.' end, c.relname, string_agg( - format(E'\t%I %s%s', + format(E' %I %s%s', a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), case when a.attnotnull then ' not null' else '' end diff --git a/sections/tools/development/db_sandbox.php b/sections/tools/development/db_sandbox.php index 5bc3f11b0..065cb6cf5 100644 --- a/sections/tools/development/db_sandbox.php +++ b/sections/tools/development/db_sandbox.php @@ -6,43 +6,44 @@ declare(strict_types=1); namespace Gazelle; -use Gazelle\Enum\SourceDB; use Gazelle\Util\Text; if (!$Viewer->permitted('admin_site_debug')) { Error403::error(); } -$src = ($_REQUEST['src'] ?? SourceDB::mysql->value) == SourceDB::mysql->value - ? SourceDB::mysql - : SourceDB::postgres; +$src = ($_REQUEST['src'] ?? Enum\SourceDB::mysql->value) == Enum\SourceDB::mysql->value + ? Enum\SourceDB::mysql + : Enum\SourceDB::postgres; -$execute = false; +$execute = false; +$query = null; +$table = null; +$textAreaRows = 8; if (isset($_GET['debug'])) { - $data = json_decode(Text::base64UrlDecode($_GET['debug']), true); + $data = json_decode(Text::base64UrlDecode($_GET['debug']), true); $query = trim($data['query']); - if ($src === SourceDB::postgres && !empty($data['args'])) { + if ($src === Enum\SourceDB::postgres && !empty($data['args'])) { $query .= "\n-- " . implode(', ', $data['args']); } $textAreaRows = max(8, substr_count($query, "\n") + 2); } elseif (isset($_GET['table'])) { - $query = (new DB())->selectQuery($_GET['table']); - $textAreaRows = max(8, substr_count($query, "\n") + 2); -} elseif (!empty($_POST['query'])) { - $query = trim($_POST['query']); - $textAreaRows = max(8, substr_count($query, "\n") + 2); - $execute = true; -} else { - $query = null; + $table = $src === Enum\SourceDB::postgres + ? new DB\PgTable($_GET['table']) + : new DB\MysqlTable($_GET['table']); $textAreaRows = 8; +} elseif (!empty($_POST['query'])) { + $query = trim($_POST['query']); + $textAreaRows = max(8, substr_count($query, "\n") + 2); + $execute = true; } $error = false; $result = []; if ($execute) { try { - if ($src == SourceDB::postgres) { + if ($src == Enum\SourceDB::postgres) { $db = new \Gazelle\DB\Pg(PG_RO_DSN); $result = $db->all($query); } else { @@ -60,5 +61,6 @@ echo $Twig->render('debug/db-sandbox.twig', [ 'rows' => $textAreaRows, 'result' => $result, 'source' => $src->value, + 'table' => $table, 'error' => $error, ]); diff --git a/templates/admin/mysql-table.twig b/templates/admin/mysql-table.twig index 60a9ac2a5..3529f0701 100644 --- a/templates/admin/mysql-table.twig +++ b/templates/admin/mysql-table.twig @@ -10,7 +10,7 @@

Mysql table {{ table.name }} definition

{{ table.definition }}
- Inspect + Inspect
diff --git a/templates/admin/pg-table.twig b/templates/admin/pg-table.twig index ca7f2e6e7..c430549c1 100644 --- a/templates/admin/pg-table.twig +++ b/templates/admin/pg-table.twig @@ -30,7 +30,7 @@ {{ table.stats.live|number_format }} {{ table.stats.dead|number_format }} - {{ (table.stats.dead == 0 + {{ (table.stats.live == 0 ? 0 : (table.stats.dead / table.stats.live) * 100)|number_format(2) }} diff --git a/templates/debug/db-sandbox.twig b/templates/debug/db-sandbox.twig index c4c0d2230..555c2343a 100644 --- a/templates/debug/db-sandbox.twig +++ b/templates/debug/db-sandbox.twig @@ -12,11 +12,13 @@ unsure how many rows will be returned. This is to avoid exhausting memory, as the resultset is buffered in memory.
If two columns have the same name (e.g. ID), only the first column will be displayed.
-

+ Source +
+
{{ table.definition }}
diff --git a/tests/phpunit/DbTest.php b/tests/phpunit/DbTest.php index 3cfa81480..e37cd2fa0 100644 --- a/tests/phpunit/DbTest.php +++ b/tests/phpunit/DbTest.php @@ -134,6 +134,14 @@ class DbTest extends TestCase { $this->assertCount(1, $warning, 'db-warning'); $this->assertEquals(1050, $warning[0]['code'], 'db-error-code'); $this->assertEquals("Table '$tableName' already exists", $warning[0]['message'], 'db-error-message'); + + $db->disableQueryLog(); + $db->prepared_query('select now()'); + $n = count($queryList); + $this->assertEquals($n, count($db->queryList()), 'db-query-log-off'); + $db->enableQueryLog(); + $db->prepared_query('select now()'); + $this->assertEquals($n + 1, count($db->queryList()), 'db-query-log-on'); } public function testGlobalStatus(): void { @@ -287,6 +295,15 @@ class DbTest extends TestCase { "); } + public function testMysqlMisc(): void { + $db = DB::DB(); + $this->assertTrue( + $db->entityExists('users_main', 'Username'), + 'db-mysql-entity-exists' + ); + $this->assertEquals($db->info(), 'mysql via TCP/IP', 'db-mysql-info'); + } + public function testMysqlTable(): void { $bad = new DB\MysqlTable('nosuchtable'); $this->assertFalse($bad->exists(), 'mysql-table-does-not-exist'); @@ -487,6 +504,17 @@ class DbTest extends TestCase { "); } + public function testMysqlPrepare(): void { + $db = DB::DB(); + $this->assertInstanceOf( + \mysqli_stmt::class, + $db->prepare('select now()'), + 'db-mysql-prepare-ok' + ); + $this->expectException(\mysqli_sql_exception::class); + $db->prepare('this is not sql'); + } + public function testPgWrapper(): void { $this->pg()->execute(" create temporary table phpunit_pg_wrapper ( @@ -515,6 +543,33 @@ class DbTest extends TestCase { ); } + public function testPgInsertCopy(): void { + $this->pg()->prepared_query(" + create temporary table phpunit_insert_copy + (num int, name text, flag bool) + "); + $output = [ + ['num' => 100, 'name' => "abc\ndef", 'flag' => true], + ['num' => -100, 'name' => "ghi\tjkl", 'flag' => false], + ['num' => 17, 'name' => null, 'flag' => 0], + ['num' => null, 'name' => null, 'flag' => null], + ]; + $input = array_map(fn ($r) => array_values($r), $output); + $this->assertTrue( + $this->pg()->insertCopy( + 'phpunit_insert_copy', + ['num', 'name', 'flag'], + $input + ), + 'pg-insert-copy', + ); + $this->assertEquals( + $output, + $this->pg()->all("select * from phpunit_insert_copy"), + 'pg-read-insert', + ); + } + public function testPgWrite(): void { $this->expectException(\PDOException::class); $this->expectExceptionMessageMatches( diff --git a/tests/phpunit/PgInfoTest.php b/tests/phpunit/PgInfoTest.php index 05064e76c..1953dc233 100644 --- a/tests/phpunit/PgInfoTest.php +++ b/tests/phpunit/PgInfoTest.php @@ -31,7 +31,7 @@ class PgInfoTest extends TestCase { Enum\Direction::descending, ); $list = $pgInfo->info(); - $this->assertEquals('public.user_warning', $list[0]['table_name'], 'pginfo-list'); + $this->assertEquals('user_warning', $list[0]['table_name'], 'pginfo-list'); } public function testPgInfoColumn(): void {