packages/ztd-query-mysql/tests/Unit/Rewrite/Partition/MySqlPartitionSelectionRewriterTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite\Partition;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Exception\UnsupportedSqlException;
11use ZtdQuery\Platform\MySql\Rewrite\Partition\MySqlPartitionSelectionRewriter;
12use ZtdQuery\Platform\MySql\Sql\Relation\MySqlSelectRelationParser;
13use ZtdQuery\Schema\Partition\TablePartitioning;
14
15#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Relation\ReferenceReader::class)]
16
17
18#[CoversClass(MySqlPartitionSelectionRewriter::class)]
19#[UsesClass(MySqlSelectRelationParser::class)]
20#[UsesClass(\ZtdQuery\Platform\MySql\Sql\MySqlLexerProfile::class)]
21#[CoversClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\SelectionReader::class)]
22#[CoversClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\SourceProjection::class)]
23final class MySqlPartitionSelectionRewriterTest extends TestCase
24{
25    public function testRewritesNamedPartitionAsFilteredDerivedTable(): void
26    {
27        $tables = [
28            'EVENTS' => [
29                'rows' => [],
30                'columns' => ['id', 'event_date'],
31                'columnTypes' => [],
32                'partitioning' => new TablePartitioning([
33                    'p2024' => 'YEAR(event_date) >= 2024 AND YEAR(event_date) < 2025',
34                ]),
35            ],
36        ];
37
38        self::assertSame(
39            'SELECT events.id FROM (SELECT * FROM events WHERE (YEAR(event_date) >= 2024 AND YEAR(event_date) < 2025)) AS events',
40            (new MySqlPartitionSelectionRewriter())->rewrite(
41                'SELECT events.id FROM events PARTITION (p2024)',
42                $tables,
43            ),
44        );
45        self::assertSame(
46            'SELECT EVENTS.id FROM (SELECT * FROM EVENTS WHERE (YEAR(event_date) >= 2024 AND YEAR(event_date) < 2025)) AS EVENTS',
47            (new MySqlPartitionSelectionRewriter())->rewrite(
48                'SELECT EVENTS.id FROM EVENTS PARTITION (p2024)',
49                $tables,
50            ),
51        );
52    }
53
54    public function testPreservesAliasesAndRewritesEachJoinSource(): void
55    {
56        $tables = [
57            'events' => [
58                'rows' => [],
59                'columns' => ['id'],
60                'columnTypes' => [],
61                'partitioning' => new TablePartitioning([
62                    'p0' => 'id < 10',
63                    'p1' => 'id >= 10',
64                ]),
65            ],
66        ];
67
68        self::assertSame(
69            'SELECT a.id FROM (SELECT * FROM `events` WHERE (id < 10) OR (id >= 10)) AS a '
70            . 'JOIN (SELECT * FROM events WHERE (id >= 10)) b ON b.id = a.id',
71            (new MySqlPartitionSelectionRewriter())->rewrite(
72                'SELECT a.id FROM app.`events` PARTITION (`p0`, p1) AS a '
73                . 'JOIN events PARTITION (p1) b ON b.id = a.id',
74                $tables,
75            ),
76        );
77    }
78
79    public function testLeavesOrdinarySelectUnchanged(): void
80    {
81        $sql = 'SELECT * FROM events';
82
83        self::assertSame($sql, (new MySqlPartitionSelectionRewriter())->rewrite($sql, []));
84    }
85
86    public function testContinuesAfterOrdinarySourceAndAddsAliasBeforeJoinClause(): void
87    {
88        $tables = [
89            'events' => [
90                'rows' => [],
91                'columns' => ['id'],
92                'columnTypes' => [],
93                'partitioning' => new TablePartitioning(['p0' => 'id < 10']),
94            ],
95        ];
96
97        self::assertSame(
98            'SELECT * FROM users JOIN (SELECT * FROM events WHERE (id < 10)) AS events ON events.id = users.id',
99            (new MySqlPartitionSelectionRewriter())->rewrite(
100                'SELECT * FROM users JOIN events PARTITION (p0) ON events.id = users.id',
101                $tables,
102            ),
103        );
104        self::assertSame(
105            'SELECT * FROM (SELECT * FROM events WHERE (id < 10)) AS events join users ON users.id = events.id',
106            (new MySqlPartitionSelectionRewriter())->rewrite(
107                'SELECT * FROM events PARTITION (p0) join users ON users.id = events.id',
108                $tables,
109            ),
110        );
111    }
112
113    public function testIgnoresClosingParenthesesBeforeThePartitionClause(): void
114    {
115        self::assertSame(
116            'SELECT COALESCE(NULL, 0) FROM (SELECT * FROM events WHERE (id < 10)) AS events',
117            (new MySqlPartitionSelectionRewriter())->rewrite(
118                'SELECT COALESCE(NULL, 0) FROM events PARTITION (p0)',
119                [
120                    'events' => [
121                        'rows' => [],
122                        'columns' => ['id'],
123                        'columnTypes' => [],
124                        'partitioning' => new TablePartitioning(['p0' => 'id < 10']),
125                    ],
126                ],
127            ),
128        );
129    }
130
131    public function testRejectsUnknownPartitionName(): void
132    {
133        self::expectException(UnsupportedSqlException::class);
134
135        (new MySqlPartitionSelectionRewriter())->rewrite(
136            'SELECT * FROM events PARTITION (missing)',
137            [
138                'events' => [
139                    'rows' => [],
140                    'columns' => ['id'],
141                    'columnTypes' => [],
142                    'partitioning' => new TablePartitioning(['p0' => 'id < 10']),
143                ],
144            ],
145        );
146    }
147
148    public function testRejectsSelectionWithoutPartitionMetadata(): void
149    {
150        self::expectException(UnsupportedSqlException::class);
151
152        (new MySqlPartitionSelectionRewriter())->rewrite(
153            'SELECT * FROM events PARTITION (p0)',
154            ['events' => ['rows' => [], 'columns' => ['id'], 'columnTypes' => []]],
155        );
156    }
157
158    public function testRejectsPartitionSelectionWithIndexHint(): void
159    {
160        self::expectException(UnsupportedSqlException::class);
161
162        (new MySqlPartitionSelectionRewriter())->rewrite(
163            'SELECT * FROM events PARTITION (p0) USE INDEX (PRIMARY)',
164            [
165                'events' => [
166                    'rows' => [],
167                    'columns' => ['id'],
168                    'columnTypes' => [],
169                    'partitioning' => new TablePartitioning(['p0' => 'id < 10']),
170                ],
171            ],
172        );
173    }
174
175    public function testRejectsEmptyPartitionList(): void
176    {
177        self::expectException(UnsupportedSqlException::class);
178
179        (new MySqlPartitionSelectionRewriter())->rewrite(
180            'SELECT * FROM events PARTITION ()',
181            [
182                'events' => [
183                    'rows' => [],
184                    'columns' => ['id'],
185                    'columnTypes' => [],
186                    'partitioning' => new TablePartitioning(['p0' => 'id < 10']),
187                ],
188            ],
189        );
190    }
191
192    public function testRejectsPartitionClauseWithoutOpeningParenthesis(): void
193    {
194        self::expectException(UnsupportedSqlException::class);
195        self::expectExceptionMessage('PARTITION selection opening parenthesis');
196
197        (new MySqlPartitionSelectionRewriter())->rewrite(
198            'SELECT * FROM events PARTITION p0',
199            ['events' => ['rows' => [], 'columns' => ['id'], 'columnTypes' => []]],
200        );
201    }
202
203    public function testRejectsBracketsInPlaceOfPartitionParentheses(): void
204    {
205        self::expectException(UnsupportedSqlException::class);
206        self::expectExceptionMessage('PARTITION selection opening parenthesis');
207
208        (new MySqlPartitionSelectionRewriter())->rewrite(
209            'SELECT * FROM events PARTITION [p0]',
210            [
211                'events' => [
212                    'rows' => [],
213                    'columns' => ['id'],
214                    'columnTypes' => [],
215                    'partitioning' => new TablePartitioning(['p0' => 'id < 10']),
216                ],
217            ],
218        );
219    }
220
221    public function testRejectsPartitionClauseWithoutClosingParenthesis(): void
222    {
223        self::expectException(UnsupportedSqlException::class);
224        self::expectExceptionMessage('PARTITION selection closing parenthesis');
225
226        (new MySqlPartitionSelectionRewriter())->rewrite(
227            'SELECT * FROM events PARTITION (p0',
228            ['events' => ['rows' => [], 'columns' => ['id'], 'columnTypes' => []]],
229        );
230    }
231
232    public function testRejectsMalformedPartitionNameList(): void
233    {
234        self::expectException(UnsupportedSqlException::class);
235
236        (new MySqlPartitionSelectionRewriter())->rewrite(
237            'SELECT * FROM events PARTITION (p0 extra)',
238            [
239                'events' => [
240                    'rows' => [],
241                    'columns' => ['id'],
242                    'columnTypes' => [],
243                    'partitioning' => new TablePartitioning(['p0' => 'id < 10']),
244                ],
245            ],
246        );
247    }
248
249    public function testRejectsForceIndexHint(): void
250    {
251        self::expectException(UnsupportedSqlException::class);
252
253        (new MySqlPartitionSelectionRewriter())->rewrite(
254            'SELECT * FROM events PARTITION (p0) force INDEX (PRIMARY)',
255            [
256                'events' => [
257                    'rows' => [],
258                    'columns' => ['id'],
259                    'columnTypes' => [],
260                    'partitioning' => new TablePartitioning(['p0' => 'id < 10']),
261                ],
262            ],
263        );
264    }
265
266    public function testRejectsIgnoreIndexHint(): void
267    {
268        self::expectException(UnsupportedSqlException::class);
269
270        (new MySqlPartitionSelectionRewriter())->rewrite(
271            'SELECT * FROM events PARTITION (p0) IGNORE INDEX (PRIMARY)',
272            [
273                'events' => [
274                    'rows' => [],
275                    'columns' => ['id'],
276                    'columnTypes' => [],
277                    'partitioning' => new TablePartitioning(['p0' => 'id < 10']),
278                ],
279            ],
280        );
281    }
282}
283