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