packages/sql-faker/tests/Unit/Sqlite/Generation/Rewrite/WindowFrameRuleTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Sqlite\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\Sqlite\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_opt');
30 $trace->expand(0, new Production([new NonTerminal('frame_bound_s')]), 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_opt');
45 $trace->expand(0, new Production([new Terminal('BETWEEN'), new NonTerminal('frame_bound_s'), new Terminal('AND'), new NonTerminal('frame_bound_e')]), 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_s');
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 /**
63 * @param list<string> $names
64 */
65 #[DataProvider('providerBoundaries')]
66 public function testRankRetainsTheFiveSourceBoundaryCategories(array $names, int $expected): void
67 {
68 $trace = new DerivationTrace('frame_bound_s');
69 $trace->expand(0, new Production(array_map(static fn (string $name): Terminal => new Terminal($name), $names)), 0);
70 self::assertSame($expected, (new WindowFrameRule())->rank($trace->terminals(), 0));
71 }
72
73 /**
74 * @return iterable<string, array{list<string>, int}>
75 */
76 public static function providerBoundaries(): iterable
77 {
78 yield 'unbounded preceding' => [['UNBOUNDED', 'PRECEDING'], 0];
79 yield 'offset preceding' => [['VALUE', 'PRECEDING'], 1];
80 yield 'current row' => [['CURRENT', 'ROW'], 2];
81 yield 'offset following' => [['VALUE', 'FOLLOWING'], 3];
82 yield 'unbounded following' => [['UNBOUNDED', 'FOLLOWING'], 4];
83 }
84
85 /**
86 * @param list<string> $start
87 * @param list<string> $end
88 * @param list<string> $expected
89 */
90 #[DataProvider('providerFrames')]
91 public function testRewritePreservesValidBoundariesAndRepairsInvalidOrdering(array $start, array $end, array $expected): void
92 {
93 $trace = new DerivationTrace('frame_opt');
94 $trace->expand(0, new Production([new Terminal('BETWEEN'), new NonTerminal('frame_bound_s'), new Terminal('AND'), new NonTerminal('frame_bound_e')]), 0);
95 $trace->expand(1, new Production(array_map(static fn (string $name): Terminal => new Terminal($name), $start)), 0);
96 $trace->expand(2 + count($start), new Production(array_map(static fn (string $name): Terminal => new Terminal($name), $end)), 0);
97 $input = $trace->terminals();
98 $rule = new WindowFrameRule();
99 $result = $rule->rewrite($input);
100 self::assertSame($expected, $result->names());
101 self::assertSame($input->original, $result->original);
102 self::assertCount(count($result->terminals), array_unique(array_map(static fn ($terminal): int => $terminal->id, $result->terminals)));
103 self::assertSame($input->productions, $result->productions);
104 self::assertSame($result, $rule->rewrite($result));
105 }
106
107 /**
108 * @return iterable<string, array{list<string>, list<string>, list<string>}>
109 */
110 public static function providerFrames(): iterable
111 {
112 yield 'equal offsets' => [['VALUE', 'FOLLOWING'], ['VALUE', 'FOLLOWING'], ['BETWEEN', 'VALUE', 'FOLLOWING', 'AND', 'VALUE', 'FOLLOWING']];
113 yield 'equal current' => [['CURRENT', 'ROW'], ['CURRENT', 'ROW'], ['BETWEEN', 'CURRENT', 'ROW', 'AND', 'CURRENT', 'ROW']];
114 yield 'preceding through current' => [['VALUE', 'PRECEDING'], ['CURRENT', 'ROW'], ['BETWEEN', 'VALUE', 'PRECEDING', 'AND', 'CURRENT', 'ROW']];
115 yield 'current through following' => [['CURRENT', 'ROW'], ['VALUE', 'FOLLOWING'], ['BETWEEN', 'CURRENT', 'ROW', 'AND', 'VALUE', 'FOLLOWING']];
116 yield 'full range' => [['UNBOUNDED', 'PRECEDING'], ['UNBOUNDED', 'FOLLOWING'], ['BETWEEN', 'UNBOUNDED', 'PRECEDING', 'AND', 'UNBOUNDED', 'FOLLOWING']];
117 yield 'following through current' => [['VALUE', 'FOLLOWING'], ['CURRENT', 'ROW'], ['BETWEEN', 'VALUE', 'FOLLOWING', 'AND', 'UNBOUNDED', 'FOLLOWING']];
118 yield 'following through preceding' => [['VALUE', 'FOLLOWING'], ['VALUE', 'PRECEDING'], ['BETWEEN', 'VALUE', 'FOLLOWING', 'AND', 'UNBOUNDED', 'FOLLOWING']];
119 }
120
121 public function testRewriteLeavesAnEmptyFrameUnchanged(): void
122 {
123 $trace = new DerivationTrace('frame_opt');
124 $trace->expand(0, new Production([]), 0);
125 $input = $trace->terminals();
126 self::assertSame($input, (new WindowFrameRule())->rewrite($input));
127 }
128}
129