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