packages/sql-faker/tests/Unit/MySql/Generation/Rewrite/Query/WindowFrameRuleTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\MySql\Generation\Rewrite\Query;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFaker\Generation\Derivation\DerivationTrace;
12use SqlFaker\Grammar\Model\NonTerminal;
13use SqlFaker\Grammar\Model\Production;
14use SqlFaker\Grammar\Model\Terminal;
15use SqlFaker\MySql\Generation\Rewrite\Query\WindowFrameRule;
16
17#[CoversClass(WindowFrameRule::class)]
18#[UsesClass(DerivationTrace::class)]
19#[UsesClass(NonTerminal::class)]
20#[UsesClass(Production::class)]
21#[UsesClass(Terminal::class)]
22#[UsesClass(\SqlFaker\Generation\Token\ProductionOccurrence::class)]
23#[UsesClass(\SqlFaker\Generation\Token\TerminalOccurrence::class)]
24#[UsesClass(\SqlFaker\Generation\Token\TerminalSequence::class)]
25final class WindowFrameRuleTest extends TestCase
26{
27    #[DataProvider('providerUnits')]
28    public function testRewriteChangesOnlyRowsFramesWithAnIntervalBoundary(string $units, string $bound, string $expected): void
29    {
30        $trace = new DerivationTrace('opt_window_frame_clause');
31        $trace->expand(0, new Production([new NonTerminal('window_frame_units'), new NonTerminal('window_frame_extent')]), 0);
32        $trace->expand(0, new Production([new Terminal($units)]), 0);
33        $trace->expand(1, new Production([new NonTerminal('window_frame_start')]), 0);
34        $trace->expand(1, new Production([new Terminal($bound), new Terminal('VALUE'), new Terminal('PRECEDING_SYM')]), 0);
35        $input = $trace->terminals();
36        $rule = new WindowFrameRule();
37        $result = $rule->rewrite($input);
38        self::assertSame([$expected, $bound, 'VALUE', 'PRECEDING_SYM'], $result->names());
39        self::assertSame($input->terminals[1], $result->terminals[1]);
40        self::assertSame($input->original, $result->original);
41        self::assertSame($input->productions, $result->productions);
42        self::assertSame($result, $rule->rewrite($result));
43    }
44
45    /**
46     * @return iterable<string, array{string, string, string}>
47     */
48    public static function providerUnits(): iterable
49    {
50        yield 'rows interval' => ['ROWS_SYM', 'INTERVAL_SYM', 'RANGE_SYM'];
51        yield 'range interval' => ['RANGE_SYM', 'INTERVAL_SYM', 'RANGE_SYM'];
52        yield 'groups interval' => ['GROUPS_SYM', 'INTERVAL_SYM', 'GROUPS_SYM'];
53        yield 'rows number' => ['ROWS_SYM', 'NUM', 'ROWS_SYM'];
54    }
55
56    public function testRewriteDoesNotUseIntervalsFromNestedFramesOrOffsetExpressions(): void
57    {
58        $trace = new DerivationTrace('opt_window_frame_clause');
59        $trace->expand(0, new Production([new NonTerminal('window_frame_units'), new NonTerminal('expr'), new NonTerminal('opt_window_frame_clause')]), 0);
60        $trace->expand(0, new Production([new Terminal('ROWS_SYM')]), 0);
61        $trace->expand(1, new Production([new Terminal('INTERVAL_SYM')]), 0);
62        $trace->expand(2, new Production([new NonTerminal('window_frame_units'), new NonTerminal('window_frame_bound')]), 0);
63        $trace->expand(2, new Production([new Terminal('ROWS_SYM')]), 0);
64        $trace->expand(3, new Production([new Terminal('INTERVAL_SYM'), new Terminal('VALUE'), new Terminal('FOLLOWING_SYM')]), 0);
65        $input = $trace->terminals();
66        $rule = new WindowFrameRule();
67        $result = $rule->rewrite($input);
68        self::assertSame(['ROWS_SYM', 'INTERVAL_SYM', 'RANGE_SYM', 'INTERVAL_SYM', 'VALUE', 'FOLLOWING_SYM'], $result->names());
69        self::assertSame($input->terminals[0], $result->terminals[0]);
70        self::assertSame($result, $rule->rewrite($result));
71    }
72
73    public function testRewriteKeepsAnAbsentFrameAndUnboundedKeepsAnAbsentBoundary(): void
74    {
75        $trace = new DerivationTrace('opt_window_frame_clause');
76        $trace->expand(0, new Production([]), 0);
77        $input = $trace->terminals();
78        $rule = new WindowFrameRule();
79        self::assertSame($input, $rule->rewrite($input));
80        self::assertSame($input, $rule->unbounded($input, 999, 'FOLLOWING_SYM'));
81    }
82
83    public function testRewriteReplacesAnEarlierEndAndRetainsAValidFrame(): void
84    {
85        $trace = new DerivationTrace('window_frame_between');
86        $trace->expand(0, new Production([new Terminal('BETWEEN_SYM'), new NonTerminal('window_frame_bound'), new Terminal('AND_SYM'), new NonTerminal('window_frame_bound')]), 1);
87        $trace->expand(1, new Production([new Terminal('CURRENT_SYM'), new Terminal('ROW_SYM')]), 2);
88        $trace->expand(4, new Production([new Terminal('VALUE'), new Terminal('PRECEDING_SYM')]), 3);
89        $result = (new WindowFrameRule())->rewrite($trace->terminals());
90        self::assertSame(['BETWEEN_SYM', 'CURRENT_SYM', 'ROW_SYM', 'AND_SYM', 'UNBOUNDED_SYM', 'FOLLOWING_SYM'], $result->names());
91        self::assertSame($result, (new WindowFrameRule())->rewrite($result));
92    }
93
94    public function testRankClassifiesEndpointsWithoutTreatingOffsetExpressionsAsSeparateBounds(): void
95    {
96        $trace = new DerivationTrace('window_frame_bound');
97        $trace->expand(0, new Production([new Terminal('UNBOUNDED_SYM'), new Terminal('PRECEDING_SYM')]), 0);
98        $rule = new WindowFrameRule();
99        self::assertSame(0, $rule->rank($trace->terminals(), 0));
100        self::assertSame(2, $rule->rank($trace->terminals(), 999));
101    }
102
103    public function testUnboundedReplacesTheEntireEndpointAndKeepsItsScope(): void
104    {
105        $trace = new DerivationTrace('window_frame_bound');
106        $trace->expand(0, new Production([new Terminal('CURRENT_P'), new Terminal('ROW_SYM')]), 2);
107        $result = (new WindowFrameRule())->unbounded($trace->terminals(), 0, 'FOLLOWING_SYM');
108        self::assertSame(['UNBOUNDED_SYM', 'FOLLOWING_SYM'], $result->names());
109        self::assertSame([0], $result->terminals[0]->ancestors);
110    }
111
112    /**
113     * @param list<string> $names
114     */
115    #[DataProvider('providerBoundaries')]
116    public function testRankRetainsTheFiveSourceBoundaryCategories(array $names, int $expected): void
117    {
118        $trace = new DerivationTrace('window_frame_bound');
119        $trace->expand(0, new Production(array_map(static fn (string $name): Terminal => new Terminal($name), $names)), 0);
120        self::assertSame($expected, (new WindowFrameRule())->rank($trace->terminals(), 0));
121    }
122
123    /**
124     * @return iterable<string, array{list<string>, int}>
125     */
126    public static function providerBoundaries(): iterable
127    {
128        yield 'unbounded preceding' => [['UNBOUNDED_SYM', 'PRECEDING_SYM'], 0];
129        yield 'offset preceding' => [['VALUE', 'PRECEDING_SYM'], 1];
130        yield 'current row' => [['CURRENT_SYM', 'ROW_SYM'], 2];
131        yield 'offset following' => [['VALUE', 'FOLLOWING_SYM'], 3];
132        yield 'unbounded following' => [['UNBOUNDED_SYM', 'FOLLOWING_SYM'], 4];
133    }
134
135    /**
136     * @param list<string> $start
137     * @param list<string> $end
138     * @param list<string> $expected
139     */
140    #[DataProvider('providerFrames')]
141    public function testRewritePreservesValidBoundariesAndRepairsInvalidOrdering(array $start, array $end, array $expected): void
142    {
143        $trace = new DerivationTrace('window_frame_between');
144        $trace->expand(0, new Production([new Terminal('BETWEEN_SYM'), new NonTerminal('window_frame_bound'), new Terminal('AND_SYM'), new NonTerminal('window_frame_bound')]), 0);
145        $trace->expand(1, new Production(array_map(static fn (string $name): Terminal => new Terminal($name), $start)), 0);
146        $trace->expand(2 + count($start), new Production(array_map(static fn (string $name): Terminal => new Terminal($name), $end)), 0);
147        $input = $trace->terminals();
148        $rule = new WindowFrameRule();
149        $result = $rule->rewrite($input);
150        self::assertSame($expected, $result->names());
151        self::assertSame($input->original, $result->original);
152        self::assertCount(count($result->terminals), array_unique(array_map(static fn ($terminal): int => $terminal->id, $result->terminals)));
153        self::assertSame($input->productions, $result->productions);
154        self::assertSame($result, $rule->rewrite($result));
155    }
156
157    /**
158     * @return iterable<string, array{list<string>, list<string>, list<string>}>
159     */
160    public static function providerFrames(): iterable
161    {
162        yield 'equal offsets' => [['VALUE', 'FOLLOWING_SYM'], ['VALUE', 'FOLLOWING_SYM'], ['BETWEEN_SYM', 'VALUE', 'FOLLOWING_SYM', 'AND_SYM', 'VALUE', 'FOLLOWING_SYM']];
163        yield 'equal current' => [['CURRENT_SYM', 'ROW_SYM'], ['CURRENT_SYM', 'ROW_SYM'], ['BETWEEN_SYM', 'CURRENT_SYM', 'ROW_SYM', 'AND_SYM', 'CURRENT_SYM', 'ROW_SYM']];
164        yield 'preceding through current' => [['VALUE', 'PRECEDING_SYM'], ['CURRENT_SYM', 'ROW_SYM'], ['BETWEEN_SYM', 'VALUE', 'PRECEDING_SYM', 'AND_SYM', 'CURRENT_SYM', 'ROW_SYM']];
165        yield 'current through following' => [['CURRENT_SYM', 'ROW_SYM'], ['VALUE', 'FOLLOWING_SYM'], ['BETWEEN_SYM', 'CURRENT_SYM', 'ROW_SYM', 'AND_SYM', 'VALUE', 'FOLLOWING_SYM']];
166        yield 'full range' => [['UNBOUNDED_SYM', 'PRECEDING_SYM'], ['UNBOUNDED_SYM', 'FOLLOWING_SYM'], ['BETWEEN_SYM', 'UNBOUNDED_SYM', 'PRECEDING_SYM', 'AND_SYM', 'UNBOUNDED_SYM', 'FOLLOWING_SYM']];
167        yield 'following through current' => [['VALUE', 'FOLLOWING_SYM'], ['CURRENT_SYM', 'ROW_SYM'], ['BETWEEN_SYM', 'VALUE', 'FOLLOWING_SYM', 'AND_SYM', 'UNBOUNDED_SYM', 'FOLLOWING_SYM']];
168        yield 'following through preceding' => [['VALUE', 'FOLLOWING_SYM'], ['VALUE', 'PRECEDING_SYM'], ['BETWEEN_SYM', 'VALUE', 'FOLLOWING_SYM', 'AND_SYM', 'UNBOUNDED_SYM', 'FOLLOWING_SYM']];
169        yield 'forbidden start' => [['UNBOUNDED_SYM', 'FOLLOWING_SYM'], ['CURRENT_SYM', 'ROW_SYM'], ['BETWEEN_SYM', 'UNBOUNDED_SYM', 'PRECEDING_SYM', 'AND_SYM', 'CURRENT_SYM', 'ROW_SYM']];
170        yield 'forbidden end' => [['UNBOUNDED_SYM', 'PRECEDING_SYM'], ['UNBOUNDED_SYM', 'PRECEDING_SYM'], ['BETWEEN_SYM', 'UNBOUNDED_SYM', 'PRECEDING_SYM', 'AND_SYM', 'UNBOUNDED_SYM', 'FOLLOWING_SYM']];
171    }
172
173    public function testRewriteLeavesAnEmptyFrameUnchanged(): void
174    {
175        $trace = new DerivationTrace('window_frame_between');
176        $trace->expand(0, new Production([]), 0);
177        $input = $trace->terminals();
178        self::assertSame($input, (new WindowFrameRule())->rewrite($input));
179    }
180}
181