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