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