From 5aec69ae2e02eeb3af055d0b0d9345370423d96b Mon Sep 17 00:00:00 2001 From: youdie006 Date: Wed, 12 Aug 2026 12:29:24 +0900 Subject: [PATCH] Accept non-reserved keyword and quoted CTE names The WITH parser only accepted a CTE name tokenized as TokenType::None, so a non-reserved keyword (e.g. data) or a backtick-quoted identifier (e.g. my_cte) was rejected with "The name of the CTE was expected." even though MySQL accepts both, making valid CTE queries fail to parse. Accept, in addition to a plain identifier, a non-reserved keyword and a backtick-quoted symbol as the CTE name; Token::value already yields the correct name (backticks stripped) in every case. Also removed the now-obsolete negated-boolean phpstan baseline entry that covered the replaced "! preg_match(...)" expression. Fixes #662 --- phpstan-baseline.neon | 6 ------ src/Statements/WithStatement.php | 12 +++++++++++- tests/Parser/WithStatementTest.php | 30 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b458d69f..427b4146 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1548,12 +1548,6 @@ parameters: count: 4 path: src/Statements/WithStatement.php - - - message: '#^Only booleans are allowed in a negated boolean, int\|false given\.$#' - identifier: booleanNot.exprNotBoolean - count: 1 - path: src/Statements/WithStatement.php - - message: '#^Only booleans are allowed in an if condition, PhpMyAdmin\\SqlParser\\Parser\|null given\.$#' identifier: if.condNotBoolean diff --git a/src/Statements/WithStatement.php b/src/Statements/WithStatement.php index 0da0a7cf..5643456e 100644 --- a/src/Statements/WithStatement.php +++ b/src/Statements/WithStatement.php @@ -10,6 +10,7 @@ use PhpMyAdmin\SqlParser\Parsers\Array2d; use PhpMyAdmin\SqlParser\Parsers\OptionsArrays; use PhpMyAdmin\SqlParser\Statement; +use PhpMyAdmin\SqlParser\Token; use PhpMyAdmin\SqlParser\TokensList; use PhpMyAdmin\SqlParser\TokenType; use PhpMyAdmin\SqlParser\Translator; @@ -108,7 +109,16 @@ public function parse(Parser $parser, TokensList $list): void } if ($state === 0) { - if ($token->type !== TokenType::None || ! preg_match('/^[a-zA-Z0-9_$]+$/', $token->token)) { + // A CTE name may be a plain identifier, a non-reserved keyword + // (e.g. `data`) or a backtick-quoted identifier. MySQL accepts + // all three, so none of them should be rejected here (see #662). + $isPlainName = $token->type === TokenType::None + && preg_match('/^[a-zA-Z0-9_$]+$/', $token->token) === 1; + $isQuotedName = $token->type === TokenType::Symbol + && ($token->flags & Token::FLAG_SYMBOL_BACKTICK) !== 0; + $isNonReservedKeyword = $token->type === TokenType::Keyword + && ($token->flags & Token::FLAG_KEYWORD_RESERVED) === 0; + if (! ($isPlainName || $isQuotedName || $isNonReservedKeyword)) { $parser->error('The name of the CTE was expected.', $token); break; } diff --git a/tests/Parser/WithStatementTest.php b/tests/Parser/WithStatementTest.php index a6c716c7..e2b241e3 100644 --- a/tests/Parser/WithStatementTest.php +++ b/tests/Parser/WithStatementTest.php @@ -7,6 +7,7 @@ use PhpMyAdmin\SqlParser\Components\WithKeyword; use PhpMyAdmin\SqlParser\Lexer; use PhpMyAdmin\SqlParser\Parser; +use PhpMyAdmin\SqlParser\Statements\WithStatement; use PhpMyAdmin\SqlParser\Tests\TestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -70,6 +71,35 @@ public function testWith(): void $this->assertEquals($expected, $parser->statements[0]->build()); } + /** @return array */ + public static function cteNameCases(): array + { + return [ + 'non-reserved keyword' => ['WITH data AS (SELECT 1) SELECT * FROM data', 'data'], + 'backtick keyword' => ['WITH `data` AS (SELECT 1) SELECT * FROM `data`', 'data'], + 'backtick identifier with space' => ['WITH `my cte` AS (SELECT 1) SELECT * FROM `my cte`', 'my cte'], + ]; + } + + #[DataProvider('cteNameCases')] + public function testWithNonReservedOrQuotedName(string $sql, string $expectedName): void + { + // https://github.com/phpmyadmin/sql-parser/issues/662 + // A CTE name may be a non-reserved keyword (e.g. "data") or a + // backtick-quoted identifier; both must be accepted instead of being + // reported as "The name of the CTE was expected." + $lexer = new Lexer($sql); + self::assertCount(0, $this->getErrorsAsArray($lexer)); + + $parser = new Parser($lexer->list); + self::assertCount(0, $this->getErrorsAsArray($parser)); + self::assertCount(1, $parser->statements); + + $statement = $parser->statements[0]; + self::assertInstanceOf(WithStatement::class, $statement); + self::assertArrayHasKey($expectedName, $statement->withers); + } + public function testWithRecursive(): void { $sql = <<<'SQL'