diff --git a/src/Components/JoinKeyword.php b/src/Components/JoinKeyword.php index ea7b882e..53e23167 100644 --- a/src/Components/JoinKeyword.php +++ b/src/Components/JoinKeyword.php @@ -6,6 +6,7 @@ use PhpMyAdmin\SqlParser\Component; use PhpMyAdmin\SqlParser\Parsers\Conditions; +use PhpMyAdmin\SqlParser\Parsers\IndexHints; use function array_search; @@ -59,29 +60,40 @@ final class JoinKeyword implements Component */ public ArrayObj|null $using = null; + /** + * Index hints + * + * @var IndexHint[] + */ + public array $indexHints = []; + /** * @see JoinKeyword::JOINS * - * @param string|null $type Join type - * @param Expression|null $expr join expression - * @param Condition[]|null $on join conditions - * @param ArrayObj|null $using columns joined + * @param string|null $type Join type + * @param Expression|null $expr join expression + * @param Condition[]|null $on join conditions + * @param ArrayObj|null $using columns joined + * @param IndexHint[] $indexHints index hints */ public function __construct( string|null $type = null, Expression|null $expr = null, array|null $on = null, ArrayObj|null $using = null, + array $indexHints = [], ) { $this->type = $type; $this->expr = $expr; $this->on = $on; $this->using = $using; + $this->indexHints = $indexHints; } public function build(): string { return array_search($this->type, self::JOINS) . ' ' . $this->expr + . ($this->indexHints !== [] ? ' ' . IndexHints::buildAll($this->indexHints) : '') . (! empty($this->on) ? ' ON ' . Conditions::buildAll($this->on) : '') . (! empty($this->using) ? ' USING ' . $this->using->build() : ''); } diff --git a/src/Parsers/JoinKeywords.php b/src/Parsers/JoinKeywords.php index 4de419d9..43fed31e 100644 --- a/src/Parsers/JoinKeywords.php +++ b/src/Parsers/JoinKeywords.php @@ -31,6 +31,7 @@ public static function parse(Parser $parser, TokensList $list, array $options = $expr = new JoinKeyword(); /** + * TODO: OLD * The state of the parser. * * Below are the states of the parser. @@ -46,6 +47,24 @@ public static function parse(Parser $parser, TokensList $list, array $options = * * 4 ----------------------[ columns ]--------------------> 0 */ + /** + * TODO: NEW + * The state of the parser. + * + * Below are the states of the parser. + * + * 0 -----------------------[ JOIN ]----------------------> 1 + * + * 1 -----------------------[ expr ]----------------------> 2 + * + * 2 -------------------[ index_hints ]-------------------> 2 + * 2 ------------------------[ ON ]-----------------------> 3 + * 2 -----------------------[ USING ]---------------------> 4 + * + * 3 --------------------[ conditions ]-------------------> 0 + * + * 4 ----------------------[ columns ]--------------------> 0 + */ $state = 0; // By design, the parser will parse first token after the keyword. @@ -90,6 +109,12 @@ public static function parse(Parser $parser, TokensList $list, array $options = case 'USING': $state = 4; break; + case 'USE': + case 'IGNORE': + case 'FORCE': + // Adding index hint on the JOIN clause. + $expr->indexHints = IndexHints::parse($parser, $list); + break; default: if (empty(JoinKeyword::JOINS[$token->keyword])) { /* Next clause is starting */ diff --git a/tests/Builder/SelectStatementTest.php b/tests/Builder/SelectStatementTest.php index 824d8b24..d150c008 100644 --- a/tests/Builder/SelectStatementTest.php +++ b/tests/Builder/SelectStatementTest.php @@ -353,4 +353,65 @@ public function testBuilderSurroundedByParanthesisWithLimit(): void $stmt->build(), ); } + + public function testBuilderSelectFromWithForceIndex(): void + { + $query = 'SELECT *' + . ' FROM uno FORCE INDEX (id)'; + $parser = new Parser($query); + $stmt = $parser->statements[0]; + + self::assertSame($query, $stmt->build()); + } + + /** + * Ensures issue #497 is fixed. + */ + public function testBuilderSelectFromJoinWithForceIndex(): void + { + $query = 'SELECT *' + . ' FROM uno' + . ' JOIN dos FORCE INDEX (two_id) ON dos.id = uno.id'; + $parser = new Parser($query); + $stmt = $parser->statements[0]; + + self::assertSame($query, $stmt->build()); + } + + /** + * Ensures issue #593 is fixed. + */ + public function testBuilderSelectFromInnerJoinWithForceIndex(): void + { + $query = 'SELECT a.id, a.name, b.order_id, b.total' + . ' FROM customers a' + . ' INNER JOIN orders b FORCE INDEX (idx_customer_id)' + . ' ON a.id = b.customer_id' + . " WHERE a.status = 'active'"; + + $parser = new Parser($query); + $stmt = $parser->statements[0]; + + $expectedQuery = 'SELECT a.id, a.name, b.order_id, b.total' + . ' FROM customers AS `a`' + . ' INNER JOIN orders AS `b` FORCE INDEX (idx_customer_id)' + . ' ON a.id = b.customer_id' + . " WHERE a.status = 'active'"; + + self::assertSame($expectedQuery, $stmt->build()); + } + + public function testBuilderSelectAllFormsOfIndexHints(): void + { + $query = 'SELECT *' + . ' FROM one USE INDEX (col1) IGNORE INDEX (col1, col2) FORCE INDEX (col1, col2, col3)' + . ' INNER JOIN two USE INDEX (col3) IGNORE INDEX (col2, col3) FORCE INDEX (col1, col2, col3)' + . ' ON one.col1 = two.col2' + . ' WHERE 1 = 1'; + + $parser = new Parser($query); + $stmt = $parser->statements[0]; + + self::assertSame($query, $stmt->build()); + } }