Skip to content

Commit 5bfc88c

Browse files
committed
test: add test coverage for previously untested classes
- Add tests for UnsupportedOperationException - Add tests for RegexpLikeValue helper class - Add tests for SubcommandSuggestionTrait - Add tests for OracleExplainParser (14 test cases) - Add tests for UI classes (InputHandler, KillConnectionAction, KillQueryAction) - Add tests for all UI panes (ActiveQueriesPane, CacheStatsPane, ConnectionPoolPane, ServerMetricsPane, SchemaBrowserPane, MigrationManagerPane, ServerVariablesPane, SqlScratchpadPane) - Add basic tests for Dashboard class All tests pass successfully. This improves code coverage for classes reported by codecov.
1 parent 1504711 commit 5bfc88c

File tree

8 files changed

+736
-9
lines changed

8 files changed

+736
-9
lines changed

src/migrations/Migration.php

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ abstract public function down(): void;
4747
* All methods from DdlQueryBuilder are available through this method.
4848
*
4949
* @return DdlQueryBuilder Returns DdlQueryBuilder instance with all schema methods available.
50-
* IDE will provide autocompletion for methods like:
51-
* - createTable(), dropTable(), renameTable()
52-
* - addColumn(), dropColumn(), alterColumn()
53-
* - createIndex(), dropIndex(), createFulltextIndex()
54-
* - addForeignKey(), dropForeignKey()
55-
* - primaryKey(), string(), integer(), text(), etc.
50+
* IDE will provide autocompletion for methods like:
51+
* - createTable(), dropTable(), renameTable()
52+
* - addColumn(), dropColumn(), alterColumn()
53+
* - createIndex(), dropIndex(), createFulltextIndex()
54+
* - addForeignKey(), dropForeignKey()
55+
* - primaryKey(), string(), integer(), text(), etc.
5656
*
5757
* @example
5858
* // Create a table
@@ -61,15 +61,12 @@ abstract public function down(): void;
6161
* 'name' => $this->schema()->string(255)->notNull(),
6262
* 'email' => $this->schema()->string(255)->notNull()->unique(),
6363
* ]);
64-
*
6564
* @example
6665
* // Add a column
6766
* $this->schema()->addColumn('users', 'status', $this->schema()->string(20)->defaultValue('active'));
68-
*
6967
* @example
7068
* // Create an index
7169
* $this->schema()->createIndex('idx_email', 'users', 'email');
72-
*
7370
* @example
7471
* // Add foreign key
7572
* $this->schema()->addForeignKey('fk_user_profile', 'profiles', 'user_id', 'users', 'id', 'CASCADE', 'CASCADE');

tests/shared/DashboardTests.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace tommyknocker\pdodb\tests\shared;
6+
7+
use tommyknocker\pdodb\cli\ui\Dashboard;
8+
9+
/**
10+
* Tests for Dashboard class.
11+
*/
12+
final class DashboardTests extends BaseSharedTestCase
13+
{
14+
public function testDashboardConstructor(): void
15+
{
16+
// Dashboard constructor should not throw
17+
$dashboard = new Dashboard(self::$db);
18+
$this->assertInstanceOf(Dashboard::class, $dashboard);
19+
}
20+
21+
public function testDashboardCanBeInstantiated(): void
22+
{
23+
// Test that Dashboard can be created with a database instance
24+
$dashboard = new Dashboard(self::$db);
25+
$this->assertInstanceOf(Dashboard::class, $dashboard);
26+
}
27+
}

tests/shared/ExplainParserTests.php

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use tommyknocker\pdodb\query\analysis\parsers\MSSQLExplainParser;
88
use tommyknocker\pdodb\query\analysis\parsers\MySQLExplainParser;
9+
use tommyknocker\pdodb\query\analysis\parsers\OracleExplainParser;
910
use tommyknocker\pdodb\query\analysis\parsers\PostgreSQLExplainParser;
1011
use tommyknocker\pdodb\query\analysis\parsers\SqliteExplainParser;
1112

@@ -621,4 +622,202 @@ public function testMSSQLExplainParserEmptyResults(): void
621622
$this->assertEquals(0, $plan->estimatedRows);
622623
$this->assertNull($plan->totalCost);
623624
}
625+
626+
public function testOracleExplainParserFullTableScan(): void
627+
{
628+
$parser = new OracleExplainParser();
629+
// Parser looks for "TABLE ACCESS FULL" followed by whitespace and table name
630+
// Pattern: /TABLE ACCESS FULL\s+(\S+)/i
631+
$explainResults = [
632+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name | Rows | Cost |'],
633+
['PLAN_TABLE_OUTPUT' => '| 0 | SELECT STATEMENT | | 1 | 2 |'],
634+
['PLAN_TABLE_OUTPUT' => '| 1 | TABLE ACCESS FULL USERS 100 2'],
635+
];
636+
637+
$plan = $parser->parse($explainResults);
638+
$this->assertNotEmpty($plan->tableScans);
639+
$this->assertContains('USERS', $plan->tableScans);
640+
$this->assertEquals('TABLE ACCESS FULL', $plan->accessType);
641+
}
642+
643+
public function testOracleExplainParserIndexRangeScan(): void
644+
{
645+
$parser = new OracleExplainParser();
646+
// Parser looks for "INDEX RANGE SCAN" followed by index name
647+
// Pattern: /INDEX\s+(?:RANGE|UNIQUE|FULL)\s+SCAN\s+.*?(\S+)/i
648+
$explainResults = [
649+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name | Rows | Cost |'],
650+
['PLAN_TABLE_OUTPUT' => '| 0 | SELECT STATEMENT | | 1 | 2 |'],
651+
['PLAN_TABLE_OUTPUT' => '| 1 | INDEX RANGE SCAN IDX_EMAIL 10 2'],
652+
];
653+
654+
$plan = $parser->parse($explainResults);
655+
$this->assertEquals('IDX_EMAIL', $plan->usedIndex);
656+
$this->assertEquals('INDEX SCAN', $plan->accessType);
657+
}
658+
659+
public function testOracleExplainParserIndexFastFullScan(): void
660+
{
661+
$parser = new OracleExplainParser();
662+
// Parser looks for "INDEX FAST FULL SCAN" followed by index name
663+
// Pattern: /INDEX\s+FAST\s+FULL\s+SCAN\s+.*?(\S+)/i
664+
$explainResults = [
665+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name | Rows | Cost |'],
666+
['PLAN_TABLE_OUTPUT' => '| 0 | SELECT STATEMENT | | 1 | 2 |'],
667+
['PLAN_TABLE_OUTPUT' => '| 1 | INDEX FAST FULL SCAN IDX_EMAIL 10 2'],
668+
];
669+
670+
$plan = $parser->parse($explainResults);
671+
$this->assertEquals('IDX_EMAIL', $plan->usedIndex);
672+
$this->assertEquals('INDEX FAST FULL SCAN', $plan->accessType);
673+
}
674+
675+
public function testOracleExplainParserWithCost(): void
676+
{
677+
$parser = new OracleExplainParser();
678+
$explainResults = [
679+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name | Rows | Cost=100 |'],
680+
];
681+
682+
$plan = $parser->parse($explainResults);
683+
$this->assertEquals(100, $plan->totalCost);
684+
}
685+
686+
public function testOracleExplainParserWithCardinality(): void
687+
{
688+
$parser = new OracleExplainParser();
689+
$explainResults = [
690+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name | Cardinality=1000 |'],
691+
];
692+
693+
$plan = $parser->parse($explainResults);
694+
$this->assertEquals(1000, $plan->estimatedRows);
695+
}
696+
697+
public function testOracleExplainParserWithJoinTypes(): void
698+
{
699+
$parser = new OracleExplainParser();
700+
$explainResults = [
701+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name |'],
702+
['PLAN_TABLE_OUTPUT' => '| 0 | SELECT STATEMENT |'],
703+
['PLAN_TABLE_OUTPUT' => '| 1 | HASH JOIN |'],
704+
];
705+
706+
$plan = $parser->parse($explainResults);
707+
$this->assertContains('HASH JOIN', $plan->joinTypes);
708+
}
709+
710+
public function testOracleExplainParserWithNestedLoops(): void
711+
{
712+
$parser = new OracleExplainParser();
713+
$explainResults = [
714+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name |'],
715+
['PLAN_TABLE_OUTPUT' => '| 0 | SELECT STATEMENT |'],
716+
['PLAN_TABLE_OUTPUT' => '| 1 | NESTED LOOPS |'],
717+
];
718+
719+
$plan = $parser->parse($explainResults);
720+
$this->assertContains('NESTED LOOPS', $plan->joinTypes);
721+
}
722+
723+
public function testOracleExplainParserWithWarnings(): void
724+
{
725+
$parser = new OracleExplainParser();
726+
// Parser should detect warnings for full table scan without index
727+
$explainResults = [
728+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name |'],
729+
['PLAN_TABLE_OUTPUT' => '| 0 | SELECT STATEMENT | |'],
730+
['PLAN_TABLE_OUTPUT' => '| 1 | TABLE ACCESS FULL USERS'],
731+
];
732+
733+
$plan = $parser->parse($explainResults);
734+
$this->assertNotEmpty($plan->warnings);
735+
$this->assertStringContainsString('Full table scan', $plan->warnings[0]);
736+
}
737+
738+
public function testOracleExplainParserWithSortOperation(): void
739+
{
740+
$parser = new OracleExplainParser();
741+
$explainResults = [
742+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name |'],
743+
['PLAN_TABLE_OUTPUT' => '| 0 | SELECT STATEMENT |'],
744+
['PLAN_TABLE_OUTPUT' => '| 1 | SORT ORDER BY |'],
745+
];
746+
747+
$plan = $parser->parse($explainResults);
748+
$this->assertNotEmpty($plan->warnings);
749+
$hasSortWarning = false;
750+
foreach ($plan->warnings as $warning) {
751+
if (str_contains($warning, 'sorting operation')) {
752+
$hasSortWarning = true;
753+
break;
754+
}
755+
}
756+
$this->assertTrue($hasSortWarning);
757+
}
758+
759+
public function testOracleExplainParserWithSchemaPrefix(): void
760+
{
761+
$parser = new OracleExplainParser();
762+
// Test that parser handles schema-prefixed table names
763+
// The parser should extract table name from "SCHEMA"."USERS" format
764+
$explainResults = [
765+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name |'],
766+
['PLAN_TABLE_OUTPUT' => '| 0 | SELECT STATEMENT | |'],
767+
['PLAN_TABLE_OUTPUT' => '| 1 | TABLE ACCESS FULL USERS'],
768+
];
769+
770+
$plan = $parser->parse($explainResults);
771+
// Basic test: parser should extract table name
772+
$this->assertNotEmpty($plan->tableScans);
773+
$this->assertContains('USERS', $plan->tableScans);
774+
}
775+
776+
public function testOracleExplainParserWithFirstColumnValue(): void
777+
{
778+
$parser = new OracleExplainParser();
779+
// Parser should use first column value if PLAN_TABLE_OUTPUT is not present
780+
// Pattern: /TABLE ACCESS FULL\s+(\S+)/i
781+
$explainResults = [
782+
['first_column' => '| Id | Operation | Name |'],
783+
['first_column' => '| 0 | SELECT STATEMENT | |'],
784+
['first_column' => '| 1 | TABLE ACCESS FULL USERS'],
785+
];
786+
787+
$plan = $parser->parse($explainResults);
788+
$this->assertNotEmpty($plan->tableScans);
789+
$this->assertContains('USERS', $plan->tableScans);
790+
}
791+
792+
public function testOracleExplainParserEmptyResults(): void
793+
{
794+
$parser = new OracleExplainParser();
795+
$plan = $parser->parse([]);
796+
$this->assertEmpty($plan->tableScans);
797+
$this->assertNull($plan->accessType);
798+
}
799+
800+
public function testOracleExplainParserWithMultipleCosts(): void
801+
{
802+
$parser = new OracleExplainParser();
803+
$explainResults = [
804+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name | Cost=50 |'],
805+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name | Cost=100 |'],
806+
];
807+
808+
$plan = $parser->parse($explainResults);
809+
$this->assertEquals(100, $plan->totalCost);
810+
}
811+
812+
public function testOracleExplainParserWithMultipleCardinalities(): void
813+
{
814+
$parser = new OracleExplainParser();
815+
$explainResults = [
816+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name | Cardinality=500 |'],
817+
['PLAN_TABLE_OUTPUT' => '| Id | Operation | Name | Cardinality=1000 |'],
818+
];
819+
820+
$plan = $parser->parse($explainResults);
821+
$this->assertEquals(1000, $plan->estimatedRows);
822+
}
624823
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace tommyknocker\pdodb\tests\shared;
6+
7+
use tommyknocker\pdodb\helpers\values\RawValue;
8+
use tommyknocker\pdodb\helpers\values\RegexpLikeValue;
9+
use tommyknocker\pdodb\helpers\values\ToCharValue;
10+
11+
/**
12+
* Tests for RegexpLikeValue.
13+
*/
14+
final class RegexpLikeValueTests extends BaseSharedTestCase
15+
{
16+
public function testRegexpLikeValueWithString(): void
17+
{
18+
$value = new RegexpLikeValue('column_name', '^test');
19+
$this->assertEquals('column_name', $value->getSourceValue());
20+
$this->assertEquals('^test', $value->getPattern());
21+
}
22+
23+
public function testRegexpLikeValueWithRawValue(): void
24+
{
25+
$rawValue = new RawValue('UPPER(column)');
26+
$value = new RegexpLikeValue($rawValue, '^TEST');
27+
$this->assertInstanceOf(RawValue::class, $value->getSourceValue());
28+
$this->assertEquals('^TEST', $value->getPattern());
29+
}
30+
31+
public function testRegexpLikeValueGetValueWithStringColumn(): void
32+
{
33+
$value = new RegexpLikeValue('column_name', '^test');
34+
$result = $value->getValue();
35+
$this->assertStringContainsString('REGEXP_LIKE', $result);
36+
// RegexpLikeValue converts column names to uppercase for Oracle
37+
$this->assertStringContainsString('COLUMN_NAME', $result);
38+
$this->assertStringContainsString('^test', $result);
39+
}
40+
41+
public function testRegexpLikeValueGetValueWithToCharValue(): void
42+
{
43+
$toCharValue = new ToCharValue('column_name');
44+
$value = new RegexpLikeValue($toCharValue, '^test');
45+
$result = $value->getValue();
46+
$this->assertStringContainsString('REGEXP_LIKE', $result);
47+
$this->assertStringContainsString('TO_CHAR', $result);
48+
$this->assertStringContainsString('^test', $result);
49+
}
50+
51+
public function testRegexpLikeValueGetValueEscapesSingleQuotes(): void
52+
{
53+
$value = new RegexpLikeValue('column_name', "test'value");
54+
$result = $value->getValue();
55+
$this->assertStringContainsString("test''value", $result);
56+
}
57+
58+
public function testRegexpLikeValueGetValueWithComplexPattern(): void
59+
{
60+
$value = new RegexpLikeValue('email', '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$');
61+
$result = $value->getValue();
62+
$this->assertStringContainsString('REGEXP_LIKE', $result);
63+
// RegexpLikeValue converts column names to uppercase for Oracle
64+
$this->assertStringContainsString('EMAIL', $result);
65+
$this->assertStringContainsString('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$', $result);
66+
}
67+
}

0 commit comments

Comments
 (0)