packages/ztd-query-mysql/tests/Unit/Schema/Partition/MySqlPartitioningParserTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Schema\Partition;
6
7use PhpMyAdmin\SqlParser\Parser;
8use PhpMyAdmin\SqlParser\Statements\CreateStatement;
9use PHPUnit\Framework\Attributes\CoversClass;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use ZtdQuery\Platform\MySql\Schema\Partition\MySqlPartitioningParser;
13
14#[CoversClass(MySqlPartitioningParser::class)]
15#[UsesClass(\ZtdQuery\Platform\MySql\Sql\MySqlLexerProfile::class)]
16#[CoversClass(\ZtdQuery\Platform\MySql\Schema\Partition\PredicateCompiler::class)]
17final class MySqlPartitioningParserTest extends TestCase
18{
19 public function testParsesRangePartitionBoundariesIncludingNullAndMaximum(): void
20 {
21 $parser = new Parser('CREATE TABLE events (event_date DATE) '
22 . 'PARTITION BY RANGE (YEAR(event_date)) ('
23 . 'PARTITION p2023 VALUES LESS THAN (2024), '
24 . 'PARTITION p2024 VALUES LESS THAN (2025), '
25 . 'PARTITION pmax VALUES LESS THAN MAXVALUE)');
26 $statement = $parser->statements[0] ?? null;
27 self::assertInstanceOf(CreateStatement::class, $statement);
28
29 $partitioning = (new MySqlPartitioningParser())->parse($statement);
30
31 self::assertNotNull($partitioning);
32 self::assertSame(
33 ['(YEAR(event_date)) IS NULL OR (YEAR(event_date)) < 2024'],
34 $partitioning->predicatesFor(['p2023']),
35 );
36 self::assertSame(
37 ['(YEAR(event_date)) >= 2024 AND (YEAR(event_date)) < 2025'],
38 $partitioning->predicatesFor(['p2024']),
39 );
40 self::assertSame(
41 ['(YEAR(event_date)) >= 2025'],
42 $partitioning->predicatesFor(['pmax']),
43 );
44 }
45
46 public function testParsesListPartitionValuesIncludingNull(): void
47 {
48 $parser = new Parser('CREATE TABLE regions (region_id INT) '
49 . 'PARTITION BY LIST (region_id) ('
50 . 'PARTITION pwest VALUES IN (NULL, 1, 2), '
51 . 'PARTITION peast VALUES IN (3, 4))');
52 $statement = $parser->statements[0] ?? null;
53 self::assertInstanceOf(CreateStatement::class, $statement);
54
55 $partitioning = (new MySqlPartitioningParser())->parse($statement);
56
57 self::assertNotNull($partitioning);
58 self::assertSame(
59 ['((region_id) IN (1, 2) OR (region_id) IS NULL)'],
60 $partitioning->predicatesFor(['pwest']),
61 );
62 self::assertSame(['(region_id) IN (3, 4)'], $partitioning->predicatesFor(['peast']));
63 }
64
65 public function testParsesNullOnlyListPartition(): void
66 {
67 $parser = new Parser('CREATE TABLE regions (region_id INT) '
68 . 'PARTITION BY LIST (region_id) (PARTITION pnull VALUES IN (NULL))');
69 $statement = $parser->statements[0] ?? null;
70 self::assertInstanceOf(CreateStatement::class, $statement);
71
72 $partitioning = (new MySqlPartitioningParser())->parse($statement);
73
74 self::assertNotNull($partitioning);
75 self::assertSame(['(region_id) IS NULL'], $partitioning->predicatesFor(['pnull']));
76 }
77
78 public function testParsesSingleMaximumRangePartition(): void
79 {
80 $parser = new Parser('CREATE TABLE events (id INT) '
81 . 'PARTITION BY RANGE (id) (PARTITION pall VALUES LESS THAN MAXVALUE)');
82 $statement = $parser->statements[0] ?? null;
83 self::assertInstanceOf(CreateStatement::class, $statement);
84
85 $partitioning = (new MySqlPartitioningParser())->parse($statement);
86
87 self::assertNotNull($partitioning);
88 self::assertSame(['TRUE'], $partitioning->predicatesFor(['pall']));
89 }
90
91 public function testMarksHashPartitionSelectionUnsupported(): void
92 {
93 $parser = new Parser('CREATE TABLE events (id INT) PARTITION BY HASH(id) PARTITIONS 4');
94 $statement = $parser->statements[0] ?? null;
95 self::assertInstanceOf(CreateStatement::class, $statement);
96
97 $partitioning = (new MySqlPartitioningParser())->parse($statement);
98
99 self::assertNotNull($partitioning);
100 self::assertNull($partitioning->predicatesFor(['p0']));
101 }
102
103 public function testMarksHashPartitionExpressionWithValidDelimitersUnsupported(): void
104 {
105 $parser = new Parser('CREATE TABLE events (id INT) PARTITION BY RANGE (id) '
106 . '(PARTITION p0 VALUES LESS THAN (10))');
107 $statement = $parser->statements[0] ?? null;
108 self::assertInstanceOf(CreateStatement::class, $statement);
109 $statement->partitionBy = 'HASH (id)';
110
111 $partitioning = (new MySqlPartitioningParser())->parse($statement);
112
113 self::assertNotNull($partitioning);
114 self::assertNull($partitioning->predicatesFor(['p0']));
115 }
116
117 public function testMarksBracketsInPlaceOfPartitionParenthesesUnsupported(): void
118 {
119 $parser = new Parser('CREATE TABLE events (id INT) PARTITION BY RANGE (id) '
120 . '(PARTITION p0 VALUES LESS THAN (10))');
121 $statement = $parser->statements[0] ?? null;
122 self::assertInstanceOf(CreateStatement::class, $statement);
123 $statement->partitionBy = 'RANGE [id]';
124
125 $partitioning = (new MySqlPartitioningParser())->parse($statement);
126
127 self::assertNotNull($partitioning);
128 self::assertNull($partitioning->predicatesFor(['p0']));
129 }
130
131 public function testMarksPartitionExpressionWithoutParenthesesUnsupported(): void
132 {
133 $parser = new Parser('CREATE TABLE events (id INT) PARTITION BY RANGE (id) '
134 . '(PARTITION p0 VALUES LESS THAN (10))');
135 $statement = $parser->statements[0] ?? null;
136 self::assertInstanceOf(CreateStatement::class, $statement);
137 $statement->partitionBy = 'RANGE id';
138
139 $partitioning = (new MySqlPartitioningParser())->parse($statement);
140
141 self::assertNotNull($partitioning);
142 self::assertNull($partitioning->predicatesFor(['p0']));
143 }
144
145 public function testMarksPartitionExpressionWithoutClosingParenthesisUnsupported(): void
146 {
147 $parser = new Parser('CREATE TABLE events (id INT) PARTITION BY RANGE (id) '
148 . '(PARTITION p0 VALUES LESS THAN (10))');
149 $statement = $parser->statements[0] ?? null;
150 self::assertInstanceOf(CreateStatement::class, $statement);
151 $statement->partitionBy = 'RANGE (id';
152
153 $partitioning = (new MySqlPartitioningParser())->parse($statement);
154
155 self::assertNotNull($partitioning);
156 self::assertNull($partitioning->predicatesFor(['p0']));
157 }
158
159 public function testReturnsNullForUnpartitionedTable(): void
160 {
161 $parser = new Parser('CREATE TABLE events (id INT)');
162 $statement = $parser->statements[0] ?? null;
163 self::assertInstanceOf(CreateStatement::class, $statement);
164
165 self::assertNull((new MySqlPartitioningParser())->parse($statement));
166 }
167
168 public function testMarksRangeWithoutDefinitionsUnsupported(): void
169 {
170 $parser = new Parser('CREATE TABLE events (id INT) PARTITION BY RANGE (id) '
171 . '(PARTITION p0 VALUES LESS THAN (10))');
172 $statement = $parser->statements[0] ?? null;
173 self::assertInstanceOf(CreateStatement::class, $statement);
174 $statement->partitions = null;
175
176 $partitioning = (new MySqlPartitioningParser())->parse($statement);
177
178 self::assertNotNull($partitioning);
179 self::assertNull($partitioning->predicatesFor(['p0']));
180 }
181
182 public function testRejectsRangeDefinitionWithListValueType(): void
183 {
184 $parser = new Parser('CREATE TABLE events (id INT) PARTITION BY RANGE (id) '
185 . '(PARTITION p0 VALUES LESS THAN (10))');
186 $statement = $parser->statements[0] ?? null;
187 self::assertInstanceOf(CreateStatement::class, $statement);
188 self::assertIsArray($statement->partitions);
189 $statement->partitions[0]->type = 'IN';
190
191 $partitioning = (new MySqlPartitioningParser())->parse($statement);
192
193 self::assertNotNull($partitioning);
194 self::assertNull($partitioning->predicatesFor(['p0']));
195 }
196
197 public function testRejectsListDefinitionWithRangeValueType(): void
198 {
199 $parser = new Parser('CREATE TABLE events (id INT) PARTITION BY LIST (id) '
200 . '(PARTITION p0 VALUES IN (10))');
201 $statement = $parser->statements[0] ?? null;
202 self::assertInstanceOf(CreateStatement::class, $statement);
203 self::assertIsArray($statement->partitions);
204 $statement->partitions[0]->type = 'LESS THAN';
205
206 $partitioning = (new MySqlPartitioningParser())->parse($statement);
207
208 self::assertNotNull($partitioning);
209 self::assertNull($partitioning->predicatesFor(['p0']));
210 }
211
212 public function testRejectsRangeColumnsWithoutGuessingTupleSemantics(): void
213 {
214 $parser = new Parser('CREATE TABLE events (id INT, created_at DATE) '
215 . 'PARTITION BY RANGE COLUMNS(id, created_at) ('
216 . "PARTITION p0 VALUES LESS THAN (10, '2025-01-01'))");
217 $statement = $parser->statements[0] ?? null;
218 self::assertInstanceOf(CreateStatement::class, $statement);
219
220 $partitioning = (new MySqlPartitioningParser())->parse($statement);
221
222 self::assertNotNull($partitioning);
223 self::assertNull($partitioning->predicatesFor(['p0']));
224 }
225}
226