packages/sql-faker/tests/Unit/PostgreSql/Generation/Rewrite/OverlapsArgumentsRuleTest.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\ProductionOccurrence;
13use SqlFaker\Generation\Token\TerminalOccurrence;
14use SqlFaker\Generation\Token\TerminalSequence;
15use SqlFaker\Grammar\Model\NonTerminal;
16use SqlFaker\Grammar\Model\Production;
17use SqlFaker\Grammar\Model\Terminal;
18use SqlFaker\PostgreSql\Generation\Rewrite\OverlapsArgumentsRule;
19
20#[CoversClass(OverlapsArgumentsRule::class)]
21#[UsesClass(DerivationTrace::class)]
22#[UsesClass(NonTerminal::class)]
23#[UsesClass(Production::class)]
24#[UsesClass(ProductionOccurrence::class)]
25#[UsesClass(Terminal::class)]
26#[UsesClass(TerminalSequence::class)]
27#[UsesClass(TerminalOccurrence::class)]
28final class OverlapsArgumentsRuleTest extends TestCase
29{
30    public function testRewriteCompletesOnlyRowsBelongingToOverlaps(): void
31    {
32        $trace = new DerivationTrace('a_expr');
33        $trace->expand(0, new Production([new NonTerminal('row'), new Terminal('OVERLAPS'), new NonTerminal('row')]), 0);
34        $trace->expand(0, new Production([new Terminal('ROW'), new Terminal('('), new Terminal(')')]), 1);
35        $trace->expand(4, new Production([new Terminal('ROW'), new Terminal('('), new Terminal(')')]), 1);
36        $input = $trace->terminals();
37        $result = (new OverlapsArgumentsRule())->rewrite($input);
38        self::assertSame(['ROW', '(', 'NULL_P', ',', 'NULL_P', ')', 'OVERLAPS', 'ROW', '(', 'NULL_P', ',', 'NULL_P', ')'], $result->names());
39        self::assertSame($input->original, $result->original);
40        self::assertSame($input->productions, $result->productions);
41        self::assertSame($result, (new OverlapsArgumentsRule())->rewrite($result));
42        $ordinary = TerminalSequence::fromNames(['ROW', '(', ')']);
43        self::assertSame($ordinary, (new OverlapsArgumentsRule())->rewrite($ordinary));
44    }
45
46    public function testElementsExcludesNestedExpressions(): void
47    {
48        $input = new TerminalSequence([], [], [], [
49            new ProductionOccurrence(0, null, 'row', 0),
50            new ProductionOccurrence(1, 0, 'expr_list', 1),
51            new ProductionOccurrence(2, 1, 'expr_list', 0),
52            new ProductionOccurrence(3, 2, 'a_expr', 0),
53            new ProductionOccurrence(4, 3, 'a_expr', 0),
54            new ProductionOccurrence(5, 1, 'a_expr', 0),
55        ]);
56        self::assertSame([3, 5], (new OverlapsArgumentsRule())->elements($input, 0));
57    }
58
59    public function testPairPreservesTwoChosenExpressionsWithoutRewriting(): void
60    {
61        $trace = new DerivationTrace('row');
62        $trace->expand(0, new Production([new Terminal('('), new NonTerminal('a_expr'), new Terminal(','), new NonTerminal('a_expr'), new Terminal(')')]), 2);
63        $trace->expand(1, new Production([new Terminal('VALUE')]), 0);
64        $trace->expand(3, new Production([new Terminal('VALUE')]), 0);
65        $input = $trace->terminals();
66        self::assertSame($input, (new OverlapsArgumentsRule())->pair($input, 0));
67    }
68
69    /**
70     * @param list<string> $expected
71     */
72    #[DataProvider('providerRows')]
73    public function testPairRetainsOnlyTheFirstTwoElementsAndTheirIdentities(TerminalSequence $input, array $expected): void
74    {
75        $rule = new OverlapsArgumentsRule();
76        $result = $rule->pair($input, 0);
77        self::assertSame($expected, $result->names());
78        self::assertSame($input->terminals[2], $result->terminals[2]);
79        self::assertSame($input->original, $result->original);
80        self::assertSame($input->productions, $result->productions);
81        self::assertSame(count($result->terminals), count(array_unique(array_map(static fn (TerminalOccurrence $terminal): int => $terminal->id, $result->terminals))));
82        self::assertSame($result, $rule->pair($result, 0));
83    }
84
85    /**
86     * @return iterable<string, array{TerminalSequence, list<string>}>
87     */
88    public static function providerRows(): iterable
89    {
90        foreach ([1, 3] as $arity) {
91            $productions = [new ProductionOccurrence(0, null, 'row', 0), new ProductionOccurrence(1, 0, 'expr_list', 0)];
92            $terminals = [new TerminalOccurrence('ROW', 10, [0], ['row']), new TerminalOccurrence('(', 11, [0], ['row'])];
93            for ($index = 0; $index < $arity; ++$index) {
94                if ($index !== 0) {
95                    $terminals[] = new TerminalOccurrence(',', 12 + $index * 2, [0, 1], ['row', 'expr_list']);
96                }
97                $productions[] = new ProductionOccurrence(2 + $index, 1, 'a_expr', 0);
98                $terminals[] = new TerminalOccurrence('VALUE_' . $index, 13 + $index * 2, [0, 1, 2 + $index], ['row', 'expr_list', 'a_expr']);
99            }
100            $terminals[] = new TerminalOccurrence(')', 30, [0], ['row']);
101            yield 'arity ' . $arity => [new TerminalSequence($terminals, $terminals, [], $productions), ['ROW', '(', 'VALUE_0', ',', $arity === 1 ? 'NULL_P' : 'VALUE_1', ')']];
102        }
103    }
104
105    public function testPairLeavesMissingRowsUntouched(): void
106    {
107        $input = TerminalSequence::fromNames(['OTHER']);
108        self::assertSame($input, (new OverlapsArgumentsRule())->pair($input, 99));
109    }
110
111    public function testRewriteIgnoresOverlapsWithoutAnExpressionScope(): void
112    {
113        $input = new TerminalSequence([new TerminalOccurrence('OVERLAPS', 1), new TerminalOccurrence('ROW', 2, [0], ['row'])], [], [], [new ProductionOccurrence(0, null, 'row', 0)]);
114        self::assertSame($input, (new OverlapsArgumentsRule())->rewrite($input));
115    }
116}
117