packages/sql-faker/tests/Unit/PostgreSql/Generation/Rewrite/FetchWithTiesRuleTest.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\Generation\Token\TerminalOccurrence;
13use SqlFaker\Generation\Token\TerminalSequence;
14use SqlFaker\Grammar\Model\NonTerminal;
15use SqlFaker\Grammar\Model\Production;
16use SqlFaker\Grammar\Model\Terminal;
17use SqlFaker\PostgreSql\Generation\Rewrite\FetchWithTiesRule;
18
19#[CoversClass(FetchWithTiesRule::class)]
20#[UsesClass(DerivationTrace::class)]
21#[UsesClass(NonTerminal::class)]
22#[UsesClass(Production::class)]
23#[UsesClass(Terminal::class)]
24#[UsesClass(TerminalOccurrence::class)]
25#[UsesClass(TerminalSequence::class)]
26#[UsesClass(\SqlFaker\Generation\Token\ProductionOccurrence::class)]
27final class FetchWithTiesRuleTest extends TestCase
28{
29    #[DataProvider('providerQueryScopes')]
30    public function testRewriteAddsOrderingForTheSameQuery(string $scope): void
31    {
32        $trace = new DerivationTrace($scope);
33        $trace->expand(0, new Production([new Terminal('SELECT'), new Terminal('ICONST'), new NonTerminal('limit_clause')]), 0);
34        $trace->expand(2, new Production([new Terminal('FETCH'), new Terminal('FIRST_P'), new Terminal('ROW'), new Terminal('WITH'), new Terminal('TIES')]), 0);
35        $input = $trace->terminals();
36        $result = (new FetchWithTiesRule())->rewrite($input);
37        self::assertSame(['SELECT', 'ICONST', 'ORDER', 'BY', 'ICONST', 'FETCH', 'FIRST_P', 'ROW', 'WITH', 'TIES'], $result->names());
38        self::assertSame($input->original, $result->original);
39        self::assertCount(count($result->terminals), array_unique(array_map(static fn ($terminal): int => $terminal->id, $result->terminals)));
40    }
41
42    #[DataProvider('providerQueryScopes')]
43    public function testOrderedPreservesAnExistingSortClause(string $scope): void
44    {
45        $terminal = new TerminalOccurrence('ORDER', 2, [0, 1], [$scope, 'sort_clause']);
46        $input = new TerminalSequence([$terminal]);
47        self::assertSame($input, (new FetchWithTiesRule())->ordered($input, 0, 0));
48    }
49
50    #[DataProvider('providerLimitWrappers')]
51    public function testOrderedPlacesOrderingBeforeOffsetAndFetch(string $wrapper): void
52    {
53        $trace = new DerivationTrace('select_no_parens');
54        $trace->expand(0, new Production([new Terminal('SELECT'), new Terminal('ICONST'), new NonTerminal('opt_sort_clause'), new NonTerminal($wrapper)]), 0);
55        $trace->expand(2, new Production([]), 0);
56        $trace->expand(2, new Production([new Terminal('OFFSET'), new Terminal('ICONST'), new NonTerminal('limit_clause')]), 0);
57        $trace->expand(4, new Production([new Terminal('FETCH'), new Terminal('FIRST_P'), new Terminal('ROW'), new Terminal('WITH'), new Terminal('TIES')]), 0);
58        $input = $trace->terminals();
59        $rule = new FetchWithTiesRule();
60        $result = $rule->ordered($input, 0, 4);
61        self::assertSame(['SELECT', 'ICONST', 'ORDER', 'BY', 'ICONST', 'OFFSET', 'ICONST', 'FETCH', 'FIRST_P', 'ROW', 'WITH', 'TIES'], $result->names());
62        self::assertSame($result->names(), $rule->rewrite($input)->names());
63        self::assertSame($input->original, $result->original);
64        self::assertSame($result, $rule->rewrite($result));
65    }
66
67    public function testPlpgsqlOrderingDoesNotSatisfyANestedSelect(): void
68    {
69        $trace = new DerivationTrace('PLpgSQL_Expr');
70        $trace->expand(0, new Production([new Terminal('('), new NonTerminal('select_no_parens'), new Terminal(')'), new NonTerminal('opt_sort_clause')]), 0);
71        $trace->expand(3, new Production([new Terminal('ORDER'), new Terminal('BY'), new Terminal('ICONST')]), 0);
72        $trace->expand(1, new Production([new Terminal('SELECT'), new Terminal('ICONST'), new NonTerminal('opt_sort_clause'), new NonTerminal('select_limit')]), 0);
73        $trace->expand(3, new Production([]), 0);
74        $trace->expand(3, new Production([new NonTerminal('limit_clause')]), 0);
75        $trace->expand(3, new Production([new Terminal('FETCH'), new Terminal('FIRST_P'), new Terminal('ROW'), new Terminal('WITH'), new Terminal('TIES')]), 0);
76        $input = $trace->terminals();
77        $rule = new FetchWithTiesRule();
78        $result = $rule->rewrite($input);
79        self::assertSame(['(', 'SELECT', 'ICONST', 'ORDER', 'BY', 'ICONST', 'FETCH', 'FIRST_P', 'ROW', 'WITH', 'TIES', ')', 'ORDER', 'BY', 'ICONST'], $result->names());
80        self::assertSame($input->original, $result->original);
81        self::assertSame($result, $rule->rewrite($result));
82    }
83
84    public function testPlpgsqlFetchDoesNotBorrowOrderingFromANestedSelect(): void
85    {
86        $trace = new DerivationTrace('PLpgSQL_Expr');
87        $trace->expand(0, new Production([new Terminal('('), new NonTerminal('select_no_parens'), new Terminal(')'), new NonTerminal('opt_sort_clause'), new NonTerminal('opt_select_limit')]), 0);
88        $trace->expand(4, new Production([new Terminal('OFFSET'), new Terminal('ICONST'), new NonTerminal('limit_clause')]), 0);
89        $trace->expand(6, new Production([new Terminal('FETCH'), new Terminal('FIRST_P'), new Terminal('ROW'), new Terminal('WITH'), new Terminal('TIES')]), 0);
90        $trace->expand(3, new Production([]), 0);
91        $trace->expand(1, new Production([new Terminal('SELECT'), new Terminal('ICONST'), new NonTerminal('sort_clause')]), 0);
92        $trace->expand(3, new Production([new Terminal('ORDER'), new Terminal('BY'), new Terminal('ICONST')]), 0);
93        $input = $trace->terminals();
94        $rule = new FetchWithTiesRule();
95        $result = $rule->rewrite($input);
96        self::assertSame(['(', 'SELECT', 'ICONST', 'ORDER', 'BY', 'ICONST', ')', 'ORDER', 'BY', 'ICONST', 'OFFSET', 'ICONST', 'FETCH', 'FIRST_P', 'ROW', 'WITH', 'TIES'], $result->names());
97        self::assertSame($input->original, $result->original);
98        self::assertSame($result, $rule->rewrite($result));
99    }
100
101    /**
102     * @return list<array{string}>
103     */
104    public static function providerQueryScopes(): array
105    {
106        return [['select_no_parens'], ['PLpgSQL_Expr']];
107    }
108
109    /**
110     * @return list<array{string}>
111     */
112    public static function providerLimitWrappers(): array
113    {
114        return [['select_limit'], ['opt_select_limit']];
115    }
116    public function testOrderedPlacesTheNewSortClauseBeforeAnExistingLockingClause(): void
117    {
118        $trace = new DerivationTrace('select_no_parens');
119        $trace->expand(0, new Production([new Terminal('SELECT'), new Terminal('ICONST'), new NonTerminal('opt_sort_clause'), new NonTerminal('for_locking_clause'), new Terminal('FETCH')]), 0);
120        $trace->expand(2, new Production([]), 0);
121        $trace->expand(2, new Production([new Terminal('FOR'), new Terminal('UPDATE')]), 0);
122        $input = $trace->terminals();
123        $result = (new FetchWithTiesRule())->ordered($input, 0, 4);
124        self::assertSame(['SELECT', 'ICONST', 'ORDER', 'BY', 'ICONST', 'FOR', 'UPDATE', 'FETCH'], $result->names());
125        self::assertSame($result, (new FetchWithTiesRule())->ordered($result, 0, 7));
126    }
127
128}
129