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