packages/ztd-query-mysql/tests/Unit/Sql/MySqlParserTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Sql;
6
7use PhpMyAdmin\SqlParser\Components\Expression;
8use PhpMyAdmin\SqlParser\Parser;
9use PhpMyAdmin\SqlParser\Statements\InsertStatement;
10use PhpMyAdmin\SqlParser\Statements\SelectStatement;
11use PHPUnit\Framework\Attributes\CoversClass;
12use PHPUnit\Framework\Attributes\UsesClass;
13use PHPUnit\Framework\TestCase;
14use ZtdQuery\Platform\MySql\Sql\MySqlParser;
15
16#[CoversClass(MySqlParser::class)]
17#[UsesClass(\ZtdQuery\Platform\MySql\Sql\MySqlLexerProfile::class)]
18#[CoversClass(\ZtdQuery\Platform\MySql\Sql\OptionalInsertIntoNormalizer::class)]
19final class MySqlParserTest extends TestCase
20{
21    public function testParseSingleLogicalStatementResolvesCompoundSelectParserArtifactsAsOneLogicalStatement(): void
22    {
23        $parser = new MySqlParser();
24
25        self::assertInstanceOf(
26            SelectStatement::class,
27            $parser->parseSingleLogicalStatement('SELECT id FROM archive UNION ALL SELECT id FROM current'),
28        );
29        self::assertInstanceOf(
30            InsertStatement::class,
31            $parser->parseSingleLogicalStatement('INSERT INTO combined (id) SELECT id FROM archive UNION ALL SELECT id FROM current'),
32        );
33    }
34
35    public function testRejectsMultipleAndNonSelectParserContinuations(): void
36    {
37        $parser = new MySqlParser();
38
39        self::assertNull($parser->parseSingleLogicalStatement('SELECT 1; SELECT 2'));
40        self::assertNull($parser->parseSingleLogicalStatement('UPDATE users SET active = 1 UNION SELECT 1'));
41        self::assertNull($parser->parseSingleLogicalStatement('INSERT INTO users SELECT 1 UNION UPDATE users SET active = 1'));
42        self::assertNull($parser->parseSingleLogicalStatement("INSERT INTO users SELECT 1 UNION SELECT 2 INTO OUTFILE '/tmp/result'"));
43        self::assertNull($parser->parseSingleLogicalStatement(''));
44    }
45
46    public function testParseAndBuildRoundTrip(): void
47    {
48        $parser = new MySqlParser();
49        $statements = $parser->parse('SELECT 1');
50
51        self::assertCount(1, $statements);
52        $sql = $statements[0]->build();
53
54        self::assertStringContainsString('SELECT 1', $sql);
55    }
56
57    public function testParseReturnsListIndexedArray(): void
58    {
59        $parser = new MySqlParser();
60        $statements = $parser->parse('SELECT 1; SELECT 2');
61
62        self::assertCount(2, $statements);
63        self::assertArrayHasKey(0, $statements);
64        self::assertArrayHasKey(1, $statements);
65    }
66
67    public function testErrorHandlerIsRestoredAfterParse(): void
68    {
69        $parser = new MySqlParser();
70
71        $handlerBefore = set_error_handler(static fn () => false);
72        restore_error_handler();
73
74        $parser->parse('SELECT 1');
75
76        $handlerAfter = set_error_handler(static fn () => false);
77        restore_error_handler();
78
79        self::assertSame($handlerBefore, $handlerAfter);
80    }
81
82    public function testParseWithLargeIntLiteralDoesNotWarn(): void
83    {
84        $parser = new MySqlParser();
85        $statements = $parser->parse('SELECT 99999999999999999999999');
86
87        self::assertCount(1, $statements);
88    }
89
90    public function testNormalizesOptionalIntoAfterModifiersAndComments(): void
91    {
92        $statements = (new MySqlParser())->parse(
93            "INSERT # lead\n HIGH_PRIORITY -- priority\n IGNORE users VALUE(/* value */ DEFAULT)",
94        );
95        $statement = $statements[0] ?? null;
96
97        self::assertInstanceOf(InsertStatement::class, $statement);
98        self::assertNotNull($statement->into);
99        self::assertInstanceOf(Expression::class, $statement->into->dest);
100        self::assertSame('users', $statement->into->dest->table);
101        self::assertSame(['DEFAULT'], $statement->values[0]->values ?? null);
102    }
103
104    public function testLeavesExplicitIntoAndNonInsertStatementsUnchanged(): void
105    {
106        $parser = new MySqlParser();
107        $explicit = $parser->parse('INSERT HIGH_PRIORITY INTO users VALUE(1)')[0] ?? null;
108        $replace = $parser->parse('REPLACE users VALUE(1)')[0] ?? null;
109        $expectedReplace = (new Parser('REPLACE users VALUE(1)'))->statements[0] ?? null;
110
111        self::assertInstanceOf(InsertStatement::class, $explicit);
112        self::assertNotNull($explicit->into);
113        self::assertInstanceOf(Expression::class, $explicit->into->dest);
114        self::assertSame('users', $explicit->into->dest->table);
115        self::assertSame($expectedReplace?->build(), $replace?->build());
116    }
117
118    public function testLeavesEmptyAndIncompleteInsertStatementsForParserValidation(): void
119    {
120        $parser = new MySqlParser();
121
122        self::assertSame([], $parser->parse(''));
123        self::assertSame('INSERT INTO ', $parser->parse('INSERT')[0]->build());
124    }
125
126    public function testNormalizesOptionalIntoWithoutModifiers(): void
127    {
128        $statement = (new MySqlParser())->parse('INSERT users VALUE(1)')[0] ?? null;
129
130        self::assertInstanceOf(InsertStatement::class, $statement);
131        self::assertNotNull($statement->into);
132        self::assertInstanceOf(Expression::class, $statement->into->dest);
133        self::assertSame('users', $statement->into->dest->table);
134    }
135
136    public function testSplitStatementsSplitsActualStatementsWithoutSplittingSetExpressions(): void
137    {
138        $parser = new MySqlParser();
139
140        self::assertSame(['SELECT 1 EXCEPT SELECT 2'], $parser->splitStatements('SELECT 1 EXCEPT SELECT 2'));
141        self::assertSame(['SELECT 1 INTERSECT SELECT 2'], $parser->splitStatements('SELECT 1 INTERSECT SELECT 2'));
142        self::assertSame(['SELECT 1', 'SELECT 2'], $parser->splitStatements('SELECT 1; SELECT 2'));
143    }
144}
145