packages/sql-faker/tests/Unit/MySql/Generation/Rewrite/Expression/ExpressionGroupingRuleTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\MySql\Generation\Rewrite\Expression;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFaker\Generation\Derivation\DerivationTrace;
11use SqlFaker\Generation\Token\ProductionOccurrence;
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\MySql\Generation\Rewrite\Expression\ExpressionGroupingRule;
18
19#[CoversClass(ExpressionGroupingRule::class)]
20#[UsesClass(DerivationTrace::class)]
21#[UsesClass(ProductionOccurrence::class)]
22#[UsesClass(TerminalOccurrence::class)]
23#[UsesClass(TerminalSequence::class)]
24#[UsesClass(NonTerminal::class)]
25#[UsesClass(Production::class)]
26#[UsesClass(Terminal::class)]
27final class ExpressionGroupingRuleTest extends TestCase
28{
29    public function testRewritePreservesBothNestedOperandsOfANonAssociativeComparison(): void
30    {
31        $trace = new DerivationTrace('a_expr');
32        $trace->expand(0, new Production([new NonTerminal('a_expr'), new Terminal('<'), new NonTerminal('a_expr')]), 0);
33        $trace->expand(0, new Production([new Terminal('1'), new Terminal('='), new Terminal('2')]), 1);
34        $trace->expand(4, new Production([new Terminal('3'), new Terminal('>'), new Terminal('4')]), 2);
35        $input = $trace->terminals();
36        $rule = new ExpressionGroupingRule(['a_expr', 'b_expr'], 'source');
37        $result = $rule->rewrite($input);
38        self::assertSame(['(', '1', '=', '2', ')', '<', '(', '3', '>', '4', ')'], $result->names());
39        self::assertSame($input->original, $result->original);
40        self::assertSame($input->productions, $result->productions);
41        self::assertSame($result->terminals, $rule->rewrite($result)->terminals);
42        self::assertSame([0, 1], $result->terminals[0]->ancestors);
43    }
44
45    public function testRewriteGroupsARestrictedOperandInsideTheGeneralExpressionFamily(): void
46    {
47        $trace = new DerivationTrace('a_expr');
48        $trace->expand(0, new Production([new Terminal('1'), new Terminal('BETWEEN'), new NonTerminal('b_expr'), new Terminal('AND'), new NonTerminal('a_expr')]), 0);
49        $trace->expand(2, new Production([new Terminal('2'), new Terminal('+'), new Terminal('3')]), 1);
50        $trace->expand(6, new Production([new Terminal('4')]), 0);
51        self::assertSame(['1', 'BETWEEN', '(', '2', '+', '3', ')', 'AND', '4'], (new ExpressionGroupingRule(['a_expr', 'b_expr'], 'source'))->rewrite($trace->terminals())->names());
52    }
53
54    public function testRewritePreservesTopLevelExpressionsAndUnrelatedNestedRules(): void
55    {
56        $trace = new DerivationTrace('stmt');
57        $trace->expand(0, new Production([new Terminal('SELECT'), new NonTerminal('a_expr')]), 0);
58        $trace->expand(1, new Production([new NonTerminal('function'), new Terminal('+'), new NonTerminal('a_expr')]), 0);
59        $trace->expand(1, new Production([new Terminal('F'), new Terminal('('), new Terminal(')')]), 0);
60        $trace->expand(5, new Production([]), 0);
61        $input = $trace->terminals();
62        self::assertSame($input, (new ExpressionGroupingRule(['a_expr'], 'source'))->rewrite($input));
63    }
64    public function testRangesIncludesNestedAncestorsAndIgnoresEmptyOccurrences(): void
65    {
66        $a = new TerminalOccurrence('1', 2, [0, 1], ['a_expr', 'a_expr']);
67        $b = new TerminalOccurrence('+', 3, [0], ['a_expr']);
68        $rule = new ExpressionGroupingRule(['a_expr'], 'source');
69        self::assertSame([0 => [0, 2], 1 => [0, 1]], $rule->ranges(new TerminalSequence([$a, $b])));
70        self::assertSame([], $rule->ranges(new TerminalSequence([])));
71        $empty = new TerminalSequence([]);
72        self::assertSame($empty, $rule->rewrite($empty));
73    }
74    public function testRewriteUsesTheConfiguredDialectTerminalNames(): void
75    {
76        $trace = new DerivationTrace('expr');
77        $trace->expand(0, new Production([new NonTerminal('expr'), new Terminal('EQ'), new Terminal('INTEGER')]), 0);
78        $trace->expand(0, new Production([new Terminal('INTEGER'), new Terminal('PLUS'), new Terminal('INTEGER')]), 1);
79        $rule = new ExpressionGroupingRule(['expr'], 'parse.y', 'LP', 'RP');
80        self::assertSame(['LP', 'INTEGER', 'PLUS', 'INTEGER', 'RP', 'EQ', 'INTEGER'], $rule->rewrite($trace->terminals())->names());
81    }
82
83    public function testRewriteRetainsNestedBoundaryProvenanceAndPreviouslyInsertedIdentities(): void
84    {
85        $a = new TerminalOccurrence('NOT', -1, [0, 1, 2], ['a_expr', 'b_expr', 'a_expr'], 'earlier');
86        $b = new TerminalOccurrence('TRUE', 10, [0, 1, 2], ['a_expr', 'b_expr', 'a_expr']);
87        $tail = new TerminalOccurrence('TAIL', 11, [0], ['a_expr']);
88        $input = new TerminalSequence([$a, $b, $tail], [$a, $b, $tail], ['earlier'], [
89            new ProductionOccurrence(0, null, 'a_expr', 0),
90            new ProductionOccurrence(1, 0, 'b_expr', 0),
91            new ProductionOccurrence(2, 1, 'a_expr', 0),
92        ]);
93        $result = (new ExpressionGroupingRule(['a_expr', 'b_expr'], 'group'))->rewrite($input);
94        self::assertSame(['(', '(', 'NOT', 'TRUE', ')', ')', 'TAIL'], $result->names());
95        $ids = array_map(static fn (TerminalOccurrence $terminal): int => $terminal->id, $result->terminals);
96        self::assertCount(count($ids), array_unique($ids));
97        self::assertLessThan(-1, max($ids[0], $ids[1], $ids[4], $ids[5]));
98        self::assertSame([0, 1], $result->terminals[0]->ancestors);
99        self::assertSame(['a_expr', 'b_expr'], $result->terminals[0]->rules);
100        self::assertSame([0, 1, 2], $result->terminals[4]->ancestors);
101        self::assertSame(['a_expr', 'b_expr', 'a_expr'], $result->terminals[4]->rules);
102        self::assertSame([0, 1], $result->terminals[5]->ancestors);
103        self::assertSame($a, $result->terminals[2]);
104        self::assertSame($b, $result->terminals[3]);
105        self::assertSame(['earlier', 'group'], $result->rewrites);
106    }
107
108    public function testRewriteContinuesAfterAnAlreadyGroupedOperand(): void
109    {
110        $left = new TerminalOccurrence('(', -1, [0, 1], ['expr', 'expr'], 'group');
111        $leftEnd = new TerminalOccurrence(')', -2, [0, 1], ['expr', 'expr'], 'group');
112        $right = new TerminalOccurrence('NOT', 10, [0, 2], ['expr', 'expr']);
113        $rightEnd = new TerminalOccurrence('TRUE', 11, [0, 2], ['expr', 'expr']);
114        $input = new TerminalSequence([$left, $leftEnd, $right, $rightEnd], [], [], [
115            new ProductionOccurrence(0, null, 'expr', 0),
116            new ProductionOccurrence(1, 0, 'expr', 0),
117            new ProductionOccurrence(2, 0, 'expr', 0),
118        ]);
119        $result = (new ExpressionGroupingRule(['expr'], 'group'))->rewrite($input);
120        self::assertSame(['(', ')', '(', 'NOT', 'TRUE', ')'], $result->names());
121        self::assertSame($left, $result->terminals[0]);
122        self::assertSame($leftEnd, $result->terminals[1]);
123    }
124}
125