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