packages/sql-faker/tests/Unit/PostgreSql/Generation/Rewrite/WindowFrameRuleTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\PostgreSql\Generation\Rewrite;
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\PostgreSql\Generation\Rewrite\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 public function testRewriteCompletesAShortFollowingFrameAndKeepsTheChosenOffset(): void
28 {
29 $trace = new DerivationTrace('frame_extent');
30 $trace->expand(0, new Production([new NonTerminal('frame_bound')]), 0);
31 $trace->expand(0, new Production([new Terminal('VALUE'), new Terminal('FOLLOWING')]), 1);
32 $input = $trace->terminals();
33 $result = (new WindowFrameRule())->rewrite($input);
34 self::assertSame(['BETWEEN', 'VALUE', 'FOLLOWING', 'AND', 'UNBOUNDED', 'FOLLOWING'], $result->names());
35 self::assertSame($input->terminals[0], $result->terminals[1]);
36 self::assertSame($input->original, $result->original);
37 self::assertCount(count($result->terminals), array_unique(array_map(static fn ($terminal): int => $terminal->id, $result->terminals)));
38 self::assertSame($input->productions, $result->productions);
39 self::assertSame($result, (new WindowFrameRule())->rewrite($result));
40 }
41
42 public function testRewriteReplacesAnEarlierEndAndRetainsAValidFrame(): void
43 {
44 $trace = new DerivationTrace('frame_extent');
45 $trace->expand(0, new Production([new Terminal('BETWEEN'), new NonTerminal('frame_bound'), new Terminal('AND'), new NonTerminal('frame_bound')]), 1);
46 $trace->expand(1, new Production([new Terminal('CURRENT'), new Terminal('ROW')]), 2);
47 $trace->expand(4, new Production([new Terminal('VALUE'), new Terminal('PRECEDING')]), 3);
48 $result = (new WindowFrameRule())->rewrite($trace->terminals());
49 self::assertSame(['BETWEEN', 'CURRENT', 'ROW', 'AND', 'UNBOUNDED', 'FOLLOWING'], $result->names());
50 self::assertSame($result, (new WindowFrameRule())->rewrite($result));
51 }
52
53 public function testRankClassifiesEndpointsWithoutTreatingOffsetExpressionsAsSeparateBounds(): void
54 {
55 $trace = new DerivationTrace('frame_bound');
56 $trace->expand(0, new Production([new Terminal('UNBOUNDED'), new Terminal('PRECEDING')]), 0);
57 $rule = new WindowFrameRule();
58 self::assertSame(0, $rule->rank($trace->terminals(), 0));
59 self::assertSame(2, $rule->rank($trace->terminals(), 999));
60 }
61
62 public function testUnboundedReplacesTheEntireEndpointAndKeepsItsScope(): void
63 {
64 $trace = new DerivationTrace('frame_bound');
65 $trace->expand(0, new Production([new Terminal('CURRENT_P'), new Terminal('ROW')]), 2);
66 $result = (new WindowFrameRule())->unbounded($trace->terminals(), 0, 'FOLLOWING');
67 self::assertSame(['UNBOUNDED', 'FOLLOWING'], $result->names());
68 self::assertSame([0], $result->terminals[0]->ancestors);
69 }
70
71 /**
72 * @param list<string> $names
73 */
74 #[DataProvider('providerBoundaries')]
75 public function testRankRetainsTheFiveSourceBoundaryCategories(array $names, int $expected): void
76 {
77 $trace = new DerivationTrace('frame_bound');
78 $trace->expand(0, new Production(array_map(static fn (string $name): Terminal => new Terminal($name), $names)), 0);
79 self::assertSame($expected, (new WindowFrameRule())->rank($trace->terminals(), 0));
80 }
81
82 /**
83 * @return iterable<string, array{list<string>, int}>
84 */
85 public static function providerBoundaries(): iterable
86 {
87 yield 'unbounded preceding' => [['UNBOUNDED', 'PRECEDING'], 0];
88 yield 'offset preceding' => [['VALUE', 'PRECEDING'], 1];
89 yield 'current row' => [['CURRENT', 'ROW'], 2];
90 yield 'offset following' => [['VALUE', 'FOLLOWING'], 3];
91 yield 'unbounded following' => [['UNBOUNDED', 'FOLLOWING'], 4];
92 }
93
94 /**
95 * @param list<string> $start
96 * @param list<string> $end
97 * @param list<string> $expected
98 */
99 #[DataProvider('providerFrames')]
100 public function testRewritePreservesValidBoundariesAndRepairsInvalidOrdering(array $start, array $end, array $expected): void
101 {
102 $trace = new DerivationTrace('frame_extent');
103 $trace->expand(0, new Production([new Terminal('BETWEEN'), new NonTerminal('frame_bound'), new Terminal('AND'), new NonTerminal('frame_bound')]), 0);
104 $trace->expand(1, new Production(array_map(static fn (string $name): Terminal => new Terminal($name), $start)), 0);
105 $trace->expand(2 + count($start), new Production(array_map(static fn (string $name): Terminal => new Terminal($name), $end)), 0);
106 $input = $trace->terminals();
107 $rule = new WindowFrameRule();
108 $result = $rule->rewrite($input);
109 self::assertSame($expected, $result->names());
110 self::assertSame($input->original, $result->original);
111 self::assertCount(count($result->terminals), array_unique(array_map(static fn ($terminal): int => $terminal->id, $result->terminals)));
112 self::assertSame($input->productions, $result->productions);
113 self::assertSame($result, $rule->rewrite($result));
114 }
115
116 /**
117 * @return iterable<string, array{list<string>, list<string>, list<string>}>
118 */
119 public static function providerFrames(): iterable
120 {
121 yield 'equal offsets' => [['VALUE', 'FOLLOWING'], ['VALUE', 'FOLLOWING'], ['BETWEEN', 'VALUE', 'FOLLOWING', 'AND', 'VALUE', 'FOLLOWING']];
122 yield 'equal current' => [['CURRENT', 'ROW'], ['CURRENT', 'ROW'], ['BETWEEN', 'CURRENT', 'ROW', 'AND', 'CURRENT', 'ROW']];
123 yield 'preceding through current' => [['VALUE', 'PRECEDING'], ['CURRENT', 'ROW'], ['BETWEEN', 'VALUE', 'PRECEDING', 'AND', 'CURRENT', 'ROW']];
124 yield 'current through following' => [['CURRENT', 'ROW'], ['VALUE', 'FOLLOWING'], ['BETWEEN', 'CURRENT', 'ROW', 'AND', 'VALUE', 'FOLLOWING']];
125 yield 'full range' => [['UNBOUNDED', 'PRECEDING'], ['UNBOUNDED', 'FOLLOWING'], ['BETWEEN', 'UNBOUNDED', 'PRECEDING', 'AND', 'UNBOUNDED', 'FOLLOWING']];
126 yield 'following through current' => [['VALUE', 'FOLLOWING'], ['CURRENT', 'ROW'], ['BETWEEN', 'VALUE', 'FOLLOWING', 'AND', 'UNBOUNDED', 'FOLLOWING']];
127 yield 'following through preceding' => [['VALUE', 'FOLLOWING'], ['VALUE', 'PRECEDING'], ['BETWEEN', 'VALUE', 'FOLLOWING', 'AND', 'UNBOUNDED', 'FOLLOWING']];
128 yield 'forbidden start' => [['UNBOUNDED', 'FOLLOWING'], ['CURRENT', 'ROW'], ['BETWEEN', 'UNBOUNDED', 'PRECEDING', 'AND', 'CURRENT', 'ROW']];
129 yield 'forbidden end' => [['UNBOUNDED', 'PRECEDING'], ['UNBOUNDED', 'PRECEDING'], ['BETWEEN', 'UNBOUNDED', 'PRECEDING', 'AND', 'UNBOUNDED', 'FOLLOWING']];
130 }
131
132 public function testRewriteLeavesAnEmptyFrameUnchanged(): void
133 {
134 $trace = new DerivationTrace('frame_extent');
135 $trace->expand(0, new Production([]), 0);
136 $input = $trace->terminals();
137 self::assertSame($input, (new WindowFrameRule())->rewrite($input));
138 }
139}
140