|
6 | 6 |
|
7 | 7 | use tommyknocker\pdodb\query\analysis\parsers\MSSQLExplainParser; |
8 | 8 | use tommyknocker\pdodb\query\analysis\parsers\MySQLExplainParser; |
| 9 | +use tommyknocker\pdodb\query\analysis\parsers\OracleExplainParser; |
9 | 10 | use tommyknocker\pdodb\query\analysis\parsers\PostgreSQLExplainParser; |
10 | 11 | use tommyknocker\pdodb\query\analysis\parsers\SqliteExplainParser; |
11 | 12 |
|
@@ -621,4 +622,202 @@ public function testMSSQLExplainParserEmptyResults(): void |
621 | 622 | $this->assertEquals(0, $plan->estimatedRows); |
622 | 623 | $this->assertNull($plan->totalCost); |
623 | 624 | } |
| 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 | + } |
624 | 823 | } |
0 commit comments