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