packages/sql-faker/tests/Unit/MySql/Generation/Rewrite/Expression/TableValueConstructorRuleTest.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\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\MySql\Generation\Rewrite\Expression\TableValueConstructorRule;
19
20#[CoversClass(TableValueConstructorRule::class)]
21#[UsesClass(DerivationTrace::class)]
22#[UsesClass(ProductionOccurrence::class)]
23#[UsesClass(TerminalOccurrence::class)]
24#[UsesClass(TerminalSequence::class)]
25#[UsesClass(NonTerminal::class)]
26#[UsesClass(Production::class)]
27#[UsesClass(Terminal::class)]
28final class TableValueConstructorRuleTest extends TestCase
29{
30    /**
31     * @param list<string> $expected
32     */
33    #[DataProvider('providerRows')]
34    public function testRewriteCompletesQueryRowsAndPreservesInsertRows(TerminalSequence $input, array $expected): void
35    {
36        $rule = new TableValueConstructorRule();
37        $result = $rule->rewrite($input);
38        self::assertSame($expected, $result->names());
39        self::assertSame($input->original, $result->original);
40        self::assertSame($input->productions, $result->productions);
41        self::assertSame($input->terminals[0], $result->terminals[0]);
42        self::assertSame($result, $rule->rewrite($result));
43        $ids = array_column($result->terminals, 'id');
44        self::assertSame($ids, array_values(array_unique($ids)));
45    }
46
47    /**
48     * @return iterable<array{TerminalSequence, list<string>}>
49     */
50    public static function providerRows(): iterable
51    {
52        $contexts = [
53            [['query_expression', 'query_expression_body', 'query_primary', 'table_value_constructor'], true],
54            [['explain_stmt', 'query_expression', 'query_primary', 'table_value_constructor'], true],
55            [['insert_query_expression', 'query_expression_with_opt_locking_clauses', 'query_expression', 'query_expression_body', 'query_primary', 'table_value_constructor'], false],
56            [['insert_query_expression', 'query_expression', 'query_expression_body', 'query_expression_parens', 'query_expression_parens', 'query_expression_with_opt_locking_clauses', 'query_expression', 'query_expression_body', 'query_primary', 'table_value_constructor'], false],
57            [['insert_query_expression', 'query_expression', 'query_specification', 'subquery', 'query_expression', 'query_primary', 'table_value_constructor'], true],
58            [['insert_query_expression', 'query_expression', 'with_clause', 'common_table_expr', 'query_expression', 'query_primary', 'table_value_constructor'], true],
59            [['ordinary'], false],
60        ];
61        foreach ($contexts as [$path, $limited]) {
62            foreach (['empty', 'default', 'expression', 'number'] as $value) {
63                foreach ([1, 3] as $count) {
64                    $trace = new DerivationTrace('root');
65                    foreach ([...$path, 'values_row_list'] as $scope) {
66                        $trace->expand(0, new Production([new NonTerminal($scope)]), 0);
67                    }
68                    $rows = [];
69                    for ($index = 0; $index < $count; ++$index) {
70                        if ($index !== 0) {
71                            $rows[] = new Terminal(',');
72                        }
73                        $rows[] = new NonTerminal('row_value_explicit');
74                    }
75                    $trace->expand(0, new Production($rows), 0);
76                    $expected = [];
77                    for ($index = 0; $index < $count; ++$index) {
78                        $offset = $index * ($value === 'empty' ? 4 : 5);
79                        $trace->expand($offset, new Production([new Terminal('ROW_SYM'), new Terminal('('), new NonTerminal('opt_values'), new Terminal(')')]), 0);
80                        $trace->expand($offset + 2, new Production($value === 'empty' ? [] : [new NonTerminal('values')]), 0);
81                        if ($value !== 'empty') {
82                            $trace->expand($offset + 2, new Production([new NonTerminal('expr_or_default')]), 0);
83                            $trace->expand($offset + 2, new Production($value === 'default' ? [new Terminal('DEFAULT_SYM')] : [new NonTerminal('expr')]), 0);
84                            if ($value !== 'default') {
85                                $trace->expand($offset + 2, new Production([new Terminal($value === 'expression' ? 'DEFAULT_SYM' : 'NUM')]), 0);
86                            }
87                        }
88                        if ($index !== 0) {
89                            $expected[] = ',';
90                        }
91                        array_push($expected, 'ROW_SYM', '(');
92                        if ($value === 'empty' && $limited || $value === 'default' && $limited) {
93                            $expected[] = 'NULL_SYM';
94                        } elseif ($value !== 'empty') {
95                            $expected[] = $value === 'number' ? 'NUM' : 'DEFAULT_SYM';
96                        }
97                        $expected[] = ')';
98                    }
99                    yield [$trace->terminals(), $expected];
100                }
101            }
102        }
103    }
104
105    public function testRewriteRecordsEmptyProductionAndReplacementProvenance(): void
106    {
107        $trace = new DerivationTrace('table_value_constructor');
108        $trace->expand(0, new Production([new NonTerminal('row_value_explicit')]), 0);
109        $trace->expand(0, new Production([new Terminal('ROW_SYM'), new Terminal('('), new NonTerminal('opt_values'), new Terminal(')')]), 0);
110        $trace->expand(2, new Production([]), 7);
111        $input = $trace->terminals();
112        $result = (new TableValueConstructorRule())->rewrite($input);
113        self::assertSame(['sql/sql_resolver.cc:resolve_table_value_constructor_values'], $result->rewrites);
114        self::assertSame('sql/sql_resolver.cc:resolve_table_value_constructor_values', $result->terminals[2]->rewrite);
115        self::assertSame([0, 1, 4], $result->terminals[2]->ancestors);
116        self::assertSame(['table_value_constructor', 'row_value_explicit', 'opt_values'], $result->terminals[2]->rules);
117        self::assertSame([2, 3], $result->range(4));
118        self::assertSame(-1, $result->terminals[2]->id);
119        self::assertSame(7, $result->productions[2]->ordinal);
120        self::assertSame($input->terminals[2], $result->terminals[3]);
121    }
122
123    public function testRewritePreservesAbsentRowsAndMissingValueProductions(): void
124    {
125        $empty = new TerminalSequence([], [], [], [new ProductionOccurrence(0, null, 'row_value_explicit', 0)]);
126        self::assertSame($empty, (new TableValueConstructorRule())->rewrite($empty));
127        $trace = new DerivationTrace('table_value_constructor');
128        $trace->expand(0, new Production([new NonTerminal('row_value_explicit')]), 0);
129        $trace->expand(0, new Production([new Terminal('ROW_SYM'), new Terminal('('), new Terminal(')')]), 0);
130        $input = $trace->terminals();
131        self::assertSame($input, (new TableValueConstructorRule())->rewrite($input));
132    }
133
134    public function testRewriteCompletesBothRowsOfAnInsertSetOperation(): void
135    {
136        $trace = new DerivationTrace('insert_query_expression');
137        $trace->expand(0, new Production([new NonTerminal('query_expression_body')]), 0);
138        $trace->expand(0, new Production([new NonTerminal('query_expression_body'), new Terminal('UNION_SYM'), new NonTerminal('query_expression_body')]), 2);
139        $trace->expand(0, new Production([new NonTerminal('table_value_constructor')]), 0);
140        $trace->expand(0, new Production([new NonTerminal('row_value_explicit')]), 0);
141        $trace->expand(0, new Production([new Terminal('ROW_SYM'), new Terminal('('), new NonTerminal('opt_values'), new Terminal(')')]), 0);
142        $trace->expand(2, new Production([]), 0);
143        $trace->expand(4, new Production([new NonTerminal('table_value_constructor')]), 0);
144        $trace->expand(4, new Production([new NonTerminal('row_value_explicit')]), 0);
145        $trace->expand(4, new Production([new Terminal('ROW_SYM'), new Terminal('('), new NonTerminal('opt_values'), new Terminal(')')]), 0);
146        $trace->expand(6, new Production([]), 0);
147        $input = $trace->terminals();
148        $rule = new TableValueConstructorRule();
149        $result = $rule->rewrite($input);
150        self::assertSame(['ROW_SYM', '(', 'NULL_SYM', ')', 'UNION_SYM', 'ROW_SYM', '(', 'NULL_SYM', ')'], $result->names());
151        self::assertSame($input->original, $result->original);
152        self::assertSame($input->productions, $result->productions);
153        self::assertSame($result, $rule->rewrite($result));
154    }
155
156    #[DataProvider('providerInsertScopes')]
157    public function testIsInsertSourceExcludesSetOperations(TerminalSequence $sequence, TerminalOccurrence $origin, bool $expected): void
158    {
159        self::assertSame($expected, (new TableValueConstructorRule())->isInsertSource($sequence, $origin));
160    }
161
162    /**
163     * @return iterable<array{TerminalSequence, TerminalOccurrence, bool}>
164     */
165    public static function providerInsertScopes(): iterable
166    {
167        foreach ([0, 1, 2, 3] as $bodyCount) {
168            foreach ([0, 10] as $parent) {
169                $productions = [new ProductionOccurrence(0, null, 'insert_query_expression', 0), new ProductionOccurrence(1, 0, 'table_value_constructor', 0)];
170                for ($index = 0; $index < $bodyCount; ++$index) {
171                    $productions[] = new ProductionOccurrence($index + 2, $parent, 'query_expression_body', 0);
172                }
173                $productions[] = new ProductionOccurrence(8, 0, 'unrelated', 0);
174                $origin = new TerminalOccurrence('ROW_SYM', 20, [0, 1], ['insert_query_expression', 'table_value_constructor']);
175                yield [new TerminalSequence([$origin], [], [], $productions), $origin, $parent !== 0 || $bodyCount < 2];
176            }
177        }
178        yield [new TerminalSequence([]), new TerminalOccurrence('ROW_SYM', 0), false];
179    }
180}
181