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