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