diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fcfc802d..5e53624c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2102,7 +2102,7 @@ parameters: - message: "#^Dynamic call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertEquals\\(\\)\\.$#" - count: 34 + count: 37 path: tests/Builder/CreateStatementTest.php - diff --git a/src/Statements/CreateStatement.php b/src/Statements/CreateStatement.php index 06100373..20161289 100644 --- a/src/Statements/CreateStatement.php +++ b/src/Statements/CreateStatement.php @@ -16,7 +16,9 @@ use PhpMyAdmin\SqlParser\Token; use PhpMyAdmin\SqlParser\TokensList; +use function ctype_space; use function is_array; +use function substr; use function trim; /** @@ -487,11 +489,16 @@ public function build() $builtStatement = $this->with->build(); } + $bodyTokens = ! empty($this->body) ? TokensList::build($this->body) : ''; + $needsSeparator = $builtStatement !== '' && $bodyTokens !== '' + && ! ctype_space(substr($builtStatement, -1)) + && ! ctype_space($bodyTokens[0]); + return 'CREATE ' . OptionsArray::build($this->options) . ' ' . Expression::build($this->name) . ' ' . $fields . ' AS ' . $builtStatement - . (! empty($this->body) ? TokensList::build($this->body) : '') . ' ' + . ($needsSeparator ? ' ' : '') . $bodyTokens . ' ' . OptionsArray::build($this->entityOptions); } elseif ($this->options->has('TRIGGER')) { return 'CREATE ' diff --git a/tests/Builder/CreateStatementTest.php b/tests/Builder/CreateStatementTest.php index c76497fd..5bc6d6f9 100644 --- a/tests/Builder/CreateStatementTest.php +++ b/tests/Builder/CreateStatementTest.php @@ -430,6 +430,49 @@ public function testBuilderView(): void ); } + public function testBuilderViewWithUnion(): void + { + // Regression test for https://github.com/phpmyadmin/phpmyadmin/issues/19692 + // The token before UNION is not a closing parenthesis, so the SELECT parser + // consumes tokens up to UNION while the trailing tokens (UNION ALL ...) end + // up in $this->body. Without a separator, build() glued them together as + // "3 = 3union all ...", producing invalid SQL. + $parser = new Parser( + 'CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER ' + . 'VIEW `v1` AS select 1 AS `a` where 3 = 3 union all (select 2 AS `a`)' + ); + $stmt = $parser->statements[0]; + + $this->assertEquals( + 'CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER ' + . 'VIEW `v1` AS SELECT 1 AS `a` WHERE 3 = 3 union all (select 2 AS `a`) ', + $stmt->build() + ); + + $parser = new Parser( + 'CREATE VIEW `v2` AS select `t`.`id` AS `id` from `t` where `t`.`id` = `t`.`id` ' + . 'union all select `u`.`id` AS `id` from `u`' + ); + $stmt = $parser->statements[0]; + + $this->assertEquals( + 'CREATE VIEW `v2` AS SELECT `t`.`id` AS `id` FROM `t` WHERE `t`.`id` = `t`.`id` ' + . 'union all select `u`.`id` AS `id` from `u` ', + $stmt->build() + ); + + // Paren-wrapped LHS: the SELECT is not parsed into $this->select, the whole + // tail lives in $this->body verbatim (whitespace preserved). Make sure the + // separator logic doesn't introduce a double space here. + $parser = new Parser('CREATE VIEW `v3` AS (select 1 AS `a`) union all (select 2 AS `a`)'); + $stmt = $parser->statements[0]; + + $this->assertEquals( + 'CREATE VIEW `v3` AS (select 1 AS `a`) union all (select 2 AS `a`) ', + $stmt->build() + ); + } + public function testBuilderViewComplex(): void { $parser = new Parser(